base
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from .common import ChangePasswordEndpoint, CSRFTokenEndpoint, SetUserPasswordEndpoint
|
||||
|
||||
from .app.check import EmailCheckEndpoint
|
||||
|
||||
from .app.email import SignInAuthEndpoint, SignUpAuthEndpoint
|
||||
from .app.github import GitHubCallbackEndpoint, GitHubOauthInitiateEndpoint
|
||||
from .app.gitlab import GitLabCallbackEndpoint, GitLabOauthInitiateEndpoint
|
||||
from .app.gitea import GiteaCallbackEndpoint, GiteaOauthInitiateEndpoint
|
||||
from .app.google import GoogleCallbackEndpoint, GoogleOauthInitiateEndpoint
|
||||
from .app.magic import MagicGenerateEndpoint, MagicSignInEndpoint, MagicSignUpEndpoint
|
||||
|
||||
from .app.signout import SignOutAuthEndpoint
|
||||
|
||||
|
||||
from .space.email import SignInAuthSpaceEndpoint, SignUpAuthSpaceEndpoint
|
||||
|
||||
from .space.github import GitHubCallbackSpaceEndpoint, GitHubOauthInitiateSpaceEndpoint
|
||||
|
||||
from .space.gitlab import GitLabCallbackSpaceEndpoint, GitLabOauthInitiateSpaceEndpoint
|
||||
|
||||
from .space.gitea import GiteaCallbackSpaceEndpoint, GiteaOauthInitiateSpaceEndpoint
|
||||
|
||||
from .space.google import GoogleCallbackSpaceEndpoint, GoogleOauthInitiateSpaceEndpoint
|
||||
|
||||
from .space.magic import (
|
||||
MagicGenerateSpaceEndpoint,
|
||||
MagicSignInSpaceEndpoint,
|
||||
MagicSignUpSpaceEndpoint,
|
||||
)
|
||||
|
||||
from .space.signout import SignOutAuthSpaceEndpoint
|
||||
|
||||
from .space.check import EmailCheckSpaceEndpoint
|
||||
|
||||
from .space.password_management import (
|
||||
ForgotPasswordSpaceEndpoint,
|
||||
ResetPasswordSpaceEndpoint,
|
||||
)
|
||||
from .app.password_management import ForgotPasswordEndpoint, ResetPasswordEndpoint
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.core.validators import validate_email
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# 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
|
||||
|
||||
## Module imports
|
||||
from plane.db.models import User
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
class EmailCheckEndpoint(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
throttle_classes = [AuthenticationThrottle]
|
||||
|
||||
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_HOST, ENABLE_MAGIC_LINK_LOGIN) = get_configuration_value(
|
||||
[
|
||||
{"key": "EMAIL_HOST", "default": os.environ.get("EMAIL_HOST", "")},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
smtp_configured = bool(EMAIL_HOST)
|
||||
is_magic_login_enabled = ENABLE_MAGIC_LINK_LOGIN == "1"
|
||||
|
||||
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)
|
||||
|
||||
# Lower the email
|
||||
email = str(email).lower().strip()
|
||||
|
||||
# Validate email
|
||||
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)
|
||||
# Check if a user already exists with the given email
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
# If existing user
|
||||
if existing_user:
|
||||
# Return response
|
||||
return Response(
|
||||
{
|
||||
"existing": True,
|
||||
"status": (
|
||||
"MAGIC_CODE"
|
||||
if existing_user.is_password_autoset and smtp_configured and is_magic_login_enabled
|
||||
else "CREDENTIAL"
|
||||
),
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
# Else return response
|
||||
return Response(
|
||||
{
|
||||
"existing": False,
|
||||
"status": ("MAGIC_CODE" if smtp_configured and is_magic_login_enabled else "CREDENTIAL"),
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -0,0 +1,239 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Django imports
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.credentials.email import EmailProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.utils.redirection_path import get_redirection_path
|
||||
from plane.authentication.utils.user_auth_workflow import post_user_auth_workflow
|
||||
from plane.db.models import User
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url
|
||||
|
||||
|
||||
class SignInAuthEndpoint(View):
|
||||
def post(self, request):
|
||||
next_path = request.POST.get("next_path")
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
# Redirection params
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
# Base URL join
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# set the referer as session to redirect after login
|
||||
email = request.POST.get("email", False)
|
||||
password = request.POST.get("password", False)
|
||||
|
||||
## Raise exception if any of the above are missing
|
||||
if not email or not password:
|
||||
# Redirection params
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["REQUIRED_EMAIL_PASSWORD_SIGN_IN"],
|
||||
error_message="REQUIRED_EMAIL_PASSWORD_SIGN_IN",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
# Next path
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Validate email
|
||||
email = email.strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL_SIGN_IN"],
|
||||
error_message="INVALID_EMAIL_SIGN_IN",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = EmailProvider(
|
||||
request=request,
|
||||
key=email,
|
||||
code=password,
|
||||
is_signup=False,
|
||||
callback=post_user_auth_workflow,
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
|
||||
# Get the safe redirect URL
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=path,
|
||||
params={},
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class SignUpAuthEndpoint(View):
|
||||
def post(self, request):
|
||||
next_path = request.POST.get("next_path")
|
||||
signup_base_url = f"{base_host(request=request, is_app=True).rstrip('/')}/sign-up"
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
# Redirection params
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=signup_base_url,
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
email = request.POST.get("email", False)
|
||||
password = request.POST.get("password", False)
|
||||
## Raise exception if any of the above are missing
|
||||
if not email or not password:
|
||||
# Redirection params
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["REQUIRED_EMAIL_PASSWORD_SIGN_UP"],
|
||||
error_message="REQUIRED_EMAIL_PASSWORD_SIGN_UP",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=signup_base_url,
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Validate the email
|
||||
email = email.strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
# Redirection params
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL_SIGN_UP"],
|
||||
error_message="INVALID_EMAIL_SIGN_UP",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=signup_base_url,
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Existing user
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
# Existing User
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=signup_base_url,
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = EmailProvider(
|
||||
request=request,
|
||||
key=email,
|
||||
code=password,
|
||||
is_signup=True,
|
||||
callback=post_user_auth_workflow,
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=path,
|
||||
params={},
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=signup_base_url,
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,107 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
import uuid
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django import
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.gitea import GiteaOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.authentication.utils.redirection_path import get_redirection_path
|
||||
from plane.authentication.utils.user_auth_workflow import post_user_auth_workflow
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GiteaOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(validate_next_path(next_path))
|
||||
|
||||
# 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",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(base_host(request=request, is_app=True), "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GiteaOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(base_host(request=request, is_app=True), "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GiteaCallbackEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
base_host = request.session.get("host")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = GiteaOAuthProvider(request=request, code=code, callback=post_user_auth_workflow)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host, path)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,106 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import uuid
|
||||
|
||||
# Django import
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.github import GitHubOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.authentication.utils.redirection_path import get_redirection_path
|
||||
from plane.authentication.utils.user_auth_workflow import post_user_auth_workflow
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url
|
||||
|
||||
|
||||
class GitHubOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
|
||||
# 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",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GitHubOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GitHubCallbackEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = GitHubOAuthProvider(request=request, code=code, callback=post_user_auth_workflow)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
if next_path:
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
|
||||
# Get the safe redirect URL
|
||||
url = get_safe_redirect_url(base_url=base_host(request=request, is_app=True), next_path=path, params={})
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,107 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import uuid
|
||||
|
||||
# Django import
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.gitlab import GitLabOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.authentication.utils.redirection_path import get_redirection_path
|
||||
from plane.authentication.utils.user_auth_workflow import post_user_auth_workflow
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url
|
||||
|
||||
|
||||
class GitLabOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
|
||||
# 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",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GitLabOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GitLabCallbackEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = GitLabOAuthProvider(request=request, code=code, callback=post_user_auth_workflow)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
|
||||
if next_path:
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = get_safe_redirect_url(base_url=base_host(request=request, is_app=True), next_path=path, params={})
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,104 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import uuid
|
||||
|
||||
# Django import
|
||||
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.user_auth_workflow import post_user_auth_workflow
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url
|
||||
|
||||
|
||||
class GoogleOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
|
||||
# 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",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GoogleOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GoogleCallbackEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
provider = GoogleOAuthProvider(request=request, code=code, callback=post_user_auth_workflow)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
url = get_safe_redirect_url(base_url=base_host(request=request, is_app=True), next_path=path, params={})
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,196 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Django imports
|
||||
from django.core.validators import validate_email
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
# 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
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.credentials.magic_code import MagicCodeProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.authentication.utils.redirection_path import get_redirection_path
|
||||
from plane.authentication.utils.user_auth_workflow import post_user_auth_workflow
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.db.models import User, Profile
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||
from plane.utils.path_validator import get_safe_redirect_url
|
||||
|
||||
|
||||
class MagicGenerateEndpoint(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
throttle_classes = [AuthenticationThrottle]
|
||||
|
||||
def post(self, request):
|
||||
# Check if instance is configured
|
||||
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", "").strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
adapter = MagicCodeProvider(request=request, key=email)
|
||||
key, token = adapter.initiate()
|
||||
# If the smtp is configured send through here
|
||||
magic_link.delay(email, key, token)
|
||||
return Response({"key": str(key)}, status=status.HTTP_200_OK)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
return Response(params, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class MagicSignInEndpoint(View):
|
||||
def post(self, request):
|
||||
# set the referer as session to redirect after login
|
||||
code = request.POST.get("code", "").strip()
|
||||
email = request.POST.get("email", "").strip().lower()
|
||||
next_path = request.POST.get("next_path")
|
||||
|
||||
if code == "" or email == "":
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED"],
|
||||
error_message="MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = MagicCodeProvider(
|
||||
request=request,
|
||||
key=f"magic_{email}",
|
||||
code=code,
|
||||
callback=post_user_auth_workflow,
|
||||
)
|
||||
user = provider.authenticate()
|
||||
profile, _ = Profile.objects.get_or_create(user=user)
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
if user.is_password_autoset and profile.is_onboarded:
|
||||
# Redirect to the home page
|
||||
path = "/"
|
||||
else:
|
||||
# Get the redirection path
|
||||
path = str(next_path) if next_path else str(get_redirection_path(user=user))
|
||||
# redirect to referer path
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=path,
|
||||
params={},
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class MagicSignUpEndpoint(View):
|
||||
def post(self, request):
|
||||
# set the referer as session to redirect after login
|
||||
code = request.POST.get("code", "").strip()
|
||||
email = request.POST.get("email", "").strip().lower()
|
||||
next_path = request.POST.get("next_path")
|
||||
|
||||
if code == "" or email == "":
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED"],
|
||||
error_message="MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing user
|
||||
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",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = MagicCodeProvider(
|
||||
request=request,
|
||||
key=f"magic_{email}",
|
||||
code=code,
|
||||
callback=post_user_auth_workflow,
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=path,
|
||||
params={},
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,174 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# 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
|
||||
|
||||
# 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.utils.password_validation import is_password_policy_valid
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||
|
||||
|
||||
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]
|
||||
|
||||
throttle_classes = [AuthenticationThrottle]
|
||||
|
||||
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,) = get_configuration_value([{"key": "EMAIL_HOST", "default": os.environ.get("EMAIL_HOST")}])
|
||||
|
||||
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 = base_host(request=request, is_app=True)
|
||||
# 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
|
||||
try:
|
||||
id = smart_str(urlsafe_base64_decode(uidb64))
|
||||
user = User.objects.get(id=id)
|
||||
except (ValueError, User.DoesNotExist):
|
||||
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_app=True),
|
||||
"accounts/reset-password?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# 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_app=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_app=True),
|
||||
"accounts/reset-password?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not is_password_policy_valid(password):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["PASSWORD_TOO_WEAK"],
|
||||
error_message="PASSWORD_TOO_WEAK",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=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_app=True),
|
||||
"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_app=True),
|
||||
"accounts/reset-password?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Django imports
|
||||
from django.views import View
|
||||
from django.contrib.auth import logout
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.utils.host import user_ip, base_host
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
class SignOutAuthEndpoint(View):
|
||||
def post(self, request):
|
||||
# Get user
|
||||
try:
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
user.last_logout_ip = user_ip(request=request)
|
||||
user.last_logout_time = timezone.now()
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
return HttpResponseRedirect(base_host(request=request, is_app=True))
|
||||
except Exception:
|
||||
return HttpResponseRedirect(base_host(request=request, is_app=True))
|
||||
@@ -0,0 +1,135 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Django imports
|
||||
from django.shortcuts import render
|
||||
|
||||
# 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
|
||||
|
||||
## Module imports
|
||||
from plane.authentication.utils.password_validation import is_password_policy_valid
|
||||
from plane.app.serializers import UserSerializer
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.db.models import User
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from django.middleware.csrf import get_token
|
||||
from plane.utils.cache import invalidate_cache
|
||||
from plane.authentication.utils.host import base_host
|
||||
|
||||
|
||||
class CSRFTokenEndpoint(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
# Generate a CSRF token
|
||||
csrf_token = get_token(request)
|
||||
# Return the CSRF token in a JSON response
|
||||
return Response({"csrf_token": str(csrf_token)}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def csrf_failure(request, reason=""):
|
||||
"""Custom CSRF failure view"""
|
||||
return render(
|
||||
request,
|
||||
"csrf_failure.html",
|
||||
{"reason": reason, "root_url": base_host(request=request)},
|
||||
)
|
||||
|
||||
|
||||
class ChangePasswordEndpoint(APIView):
|
||||
def post(self, request):
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
|
||||
# If the user password is not autoset then we need to check the old passwords
|
||||
if not user.is_password_autoset:
|
||||
old_password = request.data.get("old_password", False)
|
||||
if not old_password:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MISSING_PASSWORD"],
|
||||
error_message="MISSING_PASSWORD",
|
||||
payload={"error": "Old password is missing"},
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Get the new password
|
||||
new_password = request.data.get("new_password", False)
|
||||
|
||||
if not new_password:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MISSING_PASSWORD"],
|
||||
error_message="MISSING_PASSWORD",
|
||||
payload={"error": "Old or new password is missing"},
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# If the user password is not autoset then we need to check the old passwords
|
||||
if not user.is_password_autoset and 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)
|
||||
|
||||
if not is_password_policy_valid(new_password):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["PASSWORD_TOO_WEAK"],
|
||||
error_message="PASSWORD_TOO_WEAK",
|
||||
)
|
||||
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):
|
||||
@invalidate_cache("/api/users/me/")
|
||||
def post(self, request):
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
password = request.data.get("password", False)
|
||||
|
||||
# If the user password is not autoset then return error
|
||||
if not user.is_password_autoset:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["PASSWORD_ALREADY_SET"],
|
||||
error_message="PASSWORD_ALREADY_SET",
|
||||
payload={"error": "Your password is already set please change your password from profile"},
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Check password validation
|
||||
if not password:
|
||||
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 is_password_policy_valid(password):
|
||||
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)
|
||||
|
||||
# Set the user password
|
||||
user.set_password(password)
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
# Login the user as the session is invalidated
|
||||
user_login(user=user, request=request, is_app=True)
|
||||
# Return the user
|
||||
serializer = UserSerializer(user)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -0,0 +1,101 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.core.validators import validate_email
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# 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
|
||||
|
||||
## Module imports
|
||||
from plane.db.models import User
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
class EmailCheckSpaceEndpoint(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
throttle_classes = [AuthenticationThrottle]
|
||||
|
||||
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_HOST, ENABLE_MAGIC_LINK_LOGIN) = get_configuration_value(
|
||||
[
|
||||
{"key": "EMAIL_HOST", "default": os.environ.get("EMAIL_HOST", "")},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
smtp_configured = bool(EMAIL_HOST)
|
||||
is_magic_login_enabled = ENABLE_MAGIC_LINK_LOGIN == "1"
|
||||
|
||||
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)
|
||||
|
||||
email = str(email).lower().strip()
|
||||
# Validate email
|
||||
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)
|
||||
# Check if a user already exists with the given email
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
# If existing user
|
||||
if existing_user:
|
||||
# Return response
|
||||
return Response(
|
||||
{
|
||||
"existing": True,
|
||||
"status": (
|
||||
"MAGIC_CODE"
|
||||
if existing_user.is_password_autoset and smtp_configured and is_magic_login_enabled
|
||||
else "CREDENTIAL"
|
||||
),
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
# Else return response
|
||||
return Response(
|
||||
{
|
||||
"existing": False,
|
||||
"status": ("MAGIC_CODE" if smtp_configured and is_magic_login_enabled else "CREDENTIAL"),
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -0,0 +1,191 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Django imports
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.credentials.email import EmailProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.db.models import User
|
||||
from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path, get_allowed_hosts
|
||||
|
||||
|
||||
class SignInAuthSpaceEndpoint(View):
|
||||
def post(self, request):
|
||||
next_path = request.POST.get("next_path")
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
# Redirection params
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# set the referer as session to redirect after login
|
||||
email = request.POST.get("email", False)
|
||||
password = request.POST.get("password", False)
|
||||
|
||||
## Raise exception if any of the above are missing
|
||||
if not email or not password:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["REQUIRED_EMAIL_PASSWORD_SIGN_IN"],
|
||||
error_message="REQUIRED_EMAIL_PASSWORD_SIGN_IN",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Validate email
|
||||
email = email.strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL_SIGN_IN"],
|
||||
error_message="INVALID_EMAIL_SIGN_IN",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = EmailProvider(request=request, key=email, code=password, is_signup=False)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
next_path = validate_next_path(next_path=next_path)
|
||||
url = f"{base_host(request=request, is_space=True).rstrip('/')}{next_path}"
|
||||
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect(base_host(request=request, is_space=True))
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class SignUpAuthSpaceEndpoint(View):
|
||||
def post(self, request):
|
||||
next_path = request.POST.get("next_path")
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
# Redirection params
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
email = request.POST.get("email", False)
|
||||
password = request.POST.get("password", False)
|
||||
## Raise exception if any of the above are missing
|
||||
if not email or not password:
|
||||
# Redirection params
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["REQUIRED_EMAIL_PASSWORD_SIGN_UP"],
|
||||
error_message="REQUIRED_EMAIL_PASSWORD_SIGN_UP",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Validate the email
|
||||
email = email.strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
# Redirection params
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL_SIGN_UP"],
|
||||
error_message="INVALID_EMAIL_SIGN_UP",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Existing User
|
||||
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",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = EmailProvider(request=request, key=email, code=password, is_signup=True)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
next_path = validate_next_path(next_path=next_path)
|
||||
url = f"{base_host(request=request, is_space=True).rstrip('/')}{next_path}"
|
||||
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect(base_host(request=request, is_space=True))
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,104 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import uuid
|
||||
from urllib.parse import urlencode
|
||||
|
||||
# Django import
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.gitea import GiteaOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GiteaOauthInitiateSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
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(validate_next_path(next_path))
|
||||
|
||||
# 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",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GiteaOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GiteaCallbackSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = GiteaOAuthProvider(request=request, code=code)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# Process workspace and project invitations
|
||||
# redirect to referer path
|
||||
url = (
|
||||
f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import uuid
|
||||
|
||||
# Django import
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.github import GitHubOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path, get_allowed_hosts
|
||||
|
||||
|
||||
class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request, is_space=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
# 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",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GitHubOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GitHubCallbackSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
base_host = request.session.get("host")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = GitHubOAuthProvider(request=request, code=code)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# Process workspace and project invitations
|
||||
# redirect to referer path
|
||||
next_path = validate_next_path(next_path=next_path)
|
||||
|
||||
url = f"{base_host(request=request, is_space=True).rstrip('/')}{next_path}"
|
||||
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect(base_host(request=request, is_space=True))
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,106 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import uuid
|
||||
|
||||
# Django import
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.gitlab import GitLabOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path, get_allowed_hosts
|
||||
|
||||
|
||||
class GitLabOauthInitiateSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request, is_space=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
|
||||
# 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",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GitLabOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GitLabCallbackSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
base_host = request.session.get("host")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = GitLabOAuthProvider(request=request, code=code)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# Process workspace and project invitations
|
||||
# redirect to referer path
|
||||
next_path = validate_next_path(next_path=next_path)
|
||||
|
||||
url = f"{base_host(request=request, is_space=True).rstrip('/')}{next_path}"
|
||||
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect(base_host(request=request, is_space=True))
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,102 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import uuid
|
||||
|
||||
# Django import
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.google import GoogleOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path, get_allowed_hosts
|
||||
|
||||
|
||||
class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
request.session["host"] = base_host(request=request, is_space=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
|
||||
# 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",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GoogleOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GoogleCallbackSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
base_host = request.session.get("host")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
provider = GoogleOAuthProvider(request=request, code=code)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
next_path = validate_next_path(next_path=next_path)
|
||||
|
||||
url = f"{base_host(request=request, is_space=True).rstrip('/')}{next_path}"
|
||||
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect(base_host(request=request, is_space=True))
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True), next_path=next_path, params=params
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,170 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Django imports
|
||||
from django.core.validators import validate_email
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
# 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
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.credentials.magic_code import MagicCodeProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.db.models import User
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path, get_allowed_hosts
|
||||
|
||||
|
||||
class MagicGenerateSpaceEndpoint(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
# Check if instance is configured
|
||||
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", "").strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
adapter = MagicCodeProvider(request=request, key=email)
|
||||
key, token = adapter.initiate()
|
||||
# If the smtp is configured send through here
|
||||
magic_link.delay(email, key, token)
|
||||
return Response({"key": str(key)}, status=status.HTTP_200_OK)
|
||||
except AuthenticationException as e:
|
||||
return Response(e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class MagicSignInSpaceEndpoint(View):
|
||||
def post(self, request):
|
||||
# set the referer as session to redirect after login
|
||||
code = request.POST.get("code", "").strip()
|
||||
email = request.POST.get("email", "").strip().lower()
|
||||
next_path = request.POST.get("next_path")
|
||||
|
||||
if code == "" or email == "":
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED"],
|
||||
error_message="MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Active User
|
||||
try:
|
||||
provider = MagicCodeProvider(request=request, key=f"magic_{email}", code=code)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
next_path = validate_next_path(next_path=next_path)
|
||||
url = f"{base_host(request=request, is_space=True).rstrip('/')}{next_path}"
|
||||
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect(base_host(request=request, is_space=True))
|
||||
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class MagicSignUpSpaceEndpoint(View):
|
||||
def post(self, request):
|
||||
# set the referer as session to redirect after login
|
||||
code = request.POST.get("code", "").strip()
|
||||
email = request.POST.get("email", "").strip().lower()
|
||||
next_path = request.POST.get("next_path")
|
||||
|
||||
if code == "" or email == "":
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED"],
|
||||
error_message="MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
# Already existing
|
||||
if existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = MagicCodeProvider(request=request, key=f"magic_{email}", code=code)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
next_path = validate_next_path(next_path=next_path)
|
||||
url = f"{base_host(request=request, is_space=True).rstrip('/')}{next_path}"
|
||||
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect(base_host(request=request, is_space=True))
|
||||
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True),
|
||||
next_path=next_path,
|
||||
params=params,
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import os
|
||||
from urllib.parse import urlencode
|
||||
|
||||
# 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
|
||||
|
||||
# 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.utils.password_validation import is_password_policy_valid
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||
|
||||
|
||||
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]
|
||||
|
||||
throttle_classes = [AuthenticationThrottle]
|
||||
|
||||
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 = base_host(request=request, is_space=True)
|
||||
# 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 = f"{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 = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}" # noqa: E501
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not is_password_policy_valid(password):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["PASSWORD_TOO_WEAK"],
|
||||
error_message="PASSWORD_TOO_WEAK",
|
||||
)
|
||||
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}" # noqa: E501
|
||||
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()
|
||||
|
||||
return HttpResponseRedirect(base_host(request=request, is_space=True))
|
||||
except DjangoUnicodeDecodeError:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EXPIRED_PASSWORD_TOKEN"],
|
||||
error_message="EXPIRED_PASSWORD_TOKEN",
|
||||
)
|
||||
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}" # noqa: E501
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Django imports
|
||||
from django.views import View
|
||||
from django.contrib.auth import logout
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.utils.host import base_host, user_ip
|
||||
from plane.db.models import User
|
||||
from plane.utils.path_validator import get_safe_redirect_url
|
||||
|
||||
|
||||
class SignOutAuthSpaceEndpoint(View):
|
||||
def post(self, request):
|
||||
next_path = request.POST.get("next_path")
|
||||
|
||||
# Get user
|
||||
try:
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
user.last_logout_ip = user_ip(request=request)
|
||||
user.last_logout_time = timezone.now()
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
url = get_safe_redirect_url(base_url=base_host(request=request, is_space=True), next_path=next_path)
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
url = get_safe_redirect_url(base_url=base_host(request=request, is_space=True), next_path=next_path)
|
||||
return HttpResponseRedirect(url)
|
||||
Reference in New Issue
Block a user