This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -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))