dev: instance registration (#2912)
* dev: remove auto script for registration * dev: make all of the instance admins as owners when adding a instance admin * dev: remove sign out endpoint * dev: update takeoff script to register the instance * dev: reapply instance model * dev: check none for instance configuration encryptions * dev: encrypting secrets configuration * dev: user workflow for registration in instances * dev: add email automation configuration * dev: remove unused imports * dev: reallign migrations * dev: reconfigure license engine registrations * dev: move email check to background worker * dev: add sign up * chore: signup error message * dev: updated onboarding workflows and instance setting * dev: updated template for magic login * chore: page migration changed * dev: updated migrations and authentication for license and update template for workspace invite --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
fd5b7d20a8
commit
5ccc226498
40 changed files with 4414 additions and 1493 deletions
|
|
@ -99,7 +99,6 @@ class WorkspaceViewerPermission(BasePermission):
|
|||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__gte=10,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ class UserSerializer(BaseSerializer):
|
|||
"token_updated_at",
|
||||
"is_onboarded",
|
||||
"is_bot",
|
||||
"is_password_autoset",
|
||||
"is_email_verified",
|
||||
]
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
|
|
@ -60,6 +62,8 @@ class UserMeSerializer(BaseSerializer):
|
|||
"theme",
|
||||
"last_workspace_id",
|
||||
"use_case",
|
||||
"is_password_autoset",
|
||||
"is_email_verified",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
|
@ -189,4 +193,3 @@ class ResetPasswordSerializer(serializers.Serializer):
|
|||
Serializer for password change endpoint.
|
||||
"""
|
||||
new_password = serializers.CharField(required=True)
|
||||
confirm_password = serializers.CharField(required=True)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class WorkSpaceSerializer(BaseSerializer):
|
|||
"profile",
|
||||
"spaces",
|
||||
"workspace-invitations",
|
||||
"password",
|
||||
]:
|
||||
raise serializers.ValidationError({"slug": "Slug is not valid"})
|
||||
|
||||
|
|
|
|||
|
|
@ -5,18 +5,15 @@ from rest_framework_simplejwt.views import TokenRefreshView
|
|||
|
||||
from plane.app.views import (
|
||||
# Authentication
|
||||
SignUpEndpoint,
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
MagicSignInGenerateEndpoint,
|
||||
OauthEndpoint,
|
||||
EmailCheckEndpoint,
|
||||
## End Authentication
|
||||
# Auth Extended
|
||||
ForgotPasswordEndpoint,
|
||||
VerifyEmailEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
RequestEmailVerificationEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
## End Auth Extender
|
||||
# API Tokens
|
||||
|
|
@ -27,24 +24,14 @@ from plane.app.views import (
|
|||
|
||||
urlpatterns = [
|
||||
# Social Auth
|
||||
path("email-check/", EmailCheckEndpoint.as_view(), name="email"),
|
||||
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
||||
# Auth
|
||||
path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"),
|
||||
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||
# Magic Sign In/Up
|
||||
path(
|
||||
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
||||
),
|
||||
# magic sign in
|
||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
# Email verification
|
||||
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||
path(
|
||||
"request-email-verify/",
|
||||
RequestEmailVerificationEndpoint.as_view(),
|
||||
name="request-reset-email",
|
||||
),
|
||||
# Password Manipulation
|
||||
path(
|
||||
"users/me/change-password/",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from plane.app.views import (
|
|||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
## End User
|
||||
## Workspaces
|
||||
UserWorkSpacesEndpoint,
|
||||
|
|
@ -89,5 +90,10 @@ urlpatterns = [
|
|||
UserWorkspaceDashboardEndpoint.as_view(),
|
||||
name="user-workspace-dashboard",
|
||||
),
|
||||
path(
|
||||
"users/me/set-password/",
|
||||
SetUserPasswordEndpoint.as_view(),
|
||||
name="set-password",
|
||||
),
|
||||
## End User Graph
|
||||
]
|
||||
|
|
|
|||
|
|
@ -82,20 +82,18 @@ from .issue import (
|
|||
)
|
||||
|
||||
from .auth_extended import (
|
||||
VerifyEmailEndpoint,
|
||||
RequestEmailVerificationEndpoint,
|
||||
ForgotPasswordEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
EmailCheckEndpoint,
|
||||
)
|
||||
|
||||
|
||||
from .authentication import (
|
||||
SignUpEndpoint,
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
MagicSignInGenerateEndpoint,
|
||||
)
|
||||
|
||||
from .module import (
|
||||
|
|
@ -164,4 +162,8 @@ from .exporter import ExportIssuesEndpoint
|
|||
|
||||
from .config import ConfigurationEndpoint
|
||||
|
||||
from .webhook import WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint
|
||||
from .webhook import (
|
||||
WebhookEndpoint,
|
||||
WebhookLogsEndpoint,
|
||||
WebhookSecretRegenerateEndpoint,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
## Python imports
|
||||
import jwt
|
||||
import uuid
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
|
||||
## Django imports
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
|
|
@ -8,65 +12,95 @@ from django.utils.encoding import (
|
|||
smart_bytes,
|
||||
DjangoUnicodeDecodeError,
|
||||
)
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
||||
from django.core.validators import validate_email
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.conf import settings
|
||||
|
||||
## Third Party Imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
## Module imports
|
||||
from . import BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
ChangePasswordSerializer,
|
||||
ResetPasswordSerializer,
|
||||
UserSerializer,
|
||||
)
|
||||
from plane.db.models import User
|
||||
from plane.bgtasks.email_verification_task import email_verification
|
||||
from plane.db.models import User, WorkspaceMemberInvite
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.forgot_password_task import forgot_password
|
||||
from plane.license.models import Instance, InstanceConfiguration
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return (
|
||||
str(refresh.access_token),
|
||||
str(refresh),
|
||||
)
|
||||
|
||||
|
||||
class RequestEmailVerificationEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
token = RefreshToken.for_user(request.user).access_token
|
||||
current_site = request.META.get('HTTP_ORIGIN')
|
||||
email_verification.delay(
|
||||
request.user.first_name, request.user.email, token, current_site
|
||||
)
|
||||
return Response(
|
||||
{"message": "Email sent successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
def generate_magic_token(email):
|
||||
key = "magic_" + str(email)
|
||||
|
||||
## Generate a random token
|
||||
token = (
|
||||
"".join(random.choices(string.ascii_lowercase, k=4))
|
||||
+ "-"
|
||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
||||
+ "-"
|
||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
||||
)
|
||||
|
||||
# Initialize the redis instance
|
||||
ri = redis_instance()
|
||||
|
||||
# Check if the key already exists in python
|
||||
if ri.exists(key):
|
||||
data = json.loads(ri.get(key))
|
||||
|
||||
current_attempt = data["current_attempt"] + 1
|
||||
|
||||
if data["current_attempt"] > 2:
|
||||
return key, token, False
|
||||
|
||||
value = {
|
||||
"current_attempt": current_attempt,
|
||||
"email": email,
|
||||
"token": token,
|
||||
}
|
||||
expiry = 600
|
||||
|
||||
ri.set(key, json.dumps(value), ex=expiry)
|
||||
|
||||
else:
|
||||
value = {"current_attempt": 0, "email": email, "token": token}
|
||||
expiry = 600
|
||||
|
||||
ri.set(key, json.dumps(value), ex=expiry)
|
||||
|
||||
return key, token, True
|
||||
|
||||
|
||||
class VerifyEmailEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
token = request.GET.get("token")
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
|
||||
user = User.objects.get(id=payload["user_id"])
|
||||
def generate_password_token(user):
|
||||
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
||||
token = PasswordResetTokenGenerator().make_token(user)
|
||||
|
||||
if not user.is_email_verified:
|
||||
user.is_email_verified = True
|
||||
user.save()
|
||||
return Response(
|
||||
{"email": "Successfully activated"}, status=status.HTTP_200_OK
|
||||
)
|
||||
except jwt.ExpiredSignatureError as _indentifier:
|
||||
return Response(
|
||||
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except jwt.exceptions.DecodeError as _indentifier:
|
||||
return Response(
|
||||
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return uidb64, token
|
||||
|
||||
|
||||
class ForgotPasswordEndpoint(BaseAPIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
email = request.data.get("email")
|
||||
|
|
@ -76,7 +110,7 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
||||
token = PasswordResetTokenGenerator().make_token(user)
|
||||
|
||||
current_site = request.META.get('HTTP_ORIGIN')
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
|
|
@ -92,7 +126,7 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class ResetPasswordEndpoint(BaseAPIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
permission_classes = [AllowAny,]
|
||||
|
||||
def post(self, request, uidb64, token):
|
||||
try:
|
||||
|
|
@ -100,22 +134,26 @@ class ResetPasswordEndpoint(BaseAPIView):
|
|||
user = User.objects.get(id=id)
|
||||
if not PasswordResetTokenGenerator().check_token(user, token):
|
||||
return Response(
|
||||
{"error": "token is not valid, please check the new one"},
|
||||
{"error": "Token is invalid"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
serializer = ResetPasswordSerializer(data=request.data)
|
||||
|
||||
serializer = ResetPasswordSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
response = {
|
||||
"status": "success",
|
||||
"code": status.HTTP_200_OK,
|
||||
"message": "Password updated successfully",
|
||||
|
||||
# Generate access token for the user
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
|
||||
data = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
|
||||
return Response(response)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except DjangoUnicodeDecodeError as indentifier:
|
||||
|
|
@ -138,6 +176,208 @@ class ChangePasswordEndpoint(BaseAPIView):
|
|||
)
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
return Response({"message": "Password updated successfully"}, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
{"message": "Password updated successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class SetUserPasswordEndpoint(BaseAPIView):
|
||||
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:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your password is already set please change your password from profile"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check password validation
|
||||
if not password and len(str(password)) < 8:
|
||||
return Response(
|
||||
{"error": "Password is not valid"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Set the user password
|
||||
user.set_password(password)
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
serializer = UserSerializer(user)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class EmailCheckEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
# get the email
|
||||
|
||||
# Check the instance registration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None:
|
||||
return Response(
|
||||
{"error": "Instance is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
|
||||
email = request.data.get("email", False)
|
||||
type = request.data.get("type", "magic_code")
|
||||
|
||||
if not email:
|
||||
return Response({"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# validate the email
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
return Response({"error": "Email is not valid"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Check if the user exists
|
||||
user = User.objects.filter(email=email).first()
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
|
||||
# If new user
|
||||
if user is None:
|
||||
# Create the user
|
||||
if (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
== "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=email,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
user = User.objects.create(
|
||||
email=email,
|
||||
username=uuid.uuid4().hex,
|
||||
password=make_password(uuid.uuid4().hex),
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
||||
# Update instance user count
|
||||
update_user_instance_user_count.delay()
|
||||
|
||||
# Case when the user selects magic code
|
||||
if type == "magic_code":
|
||||
if not bool(get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN")),
|
||||
):
|
||||
return Response(
|
||||
{"error": "Magic link sign in is disabled."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=True,
|
||||
)
|
||||
key, token, current_attempt = generate_magic_token(email=email)
|
||||
if not current_attempt:
|
||||
return Response({"error": "Max attempts exhausted. Please try again later."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# Trigger the email
|
||||
magic_link.delay(email, "magic_" + str(email), token, current_site)
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
# Get the uidb64 and token for the user
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
)
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=True,
|
||||
)
|
||||
# Automatically send the email
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# Existing user
|
||||
else:
|
||||
if type == "magic_code":
|
||||
## Generate a random token
|
||||
if not bool(get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN")),
|
||||
):
|
||||
return Response(
|
||||
{"error": "Magic link sign in is disabled."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
# Generate magic token
|
||||
key, token, current_attempt = generate_magic_token(email=email)
|
||||
if not current_attempt:
|
||||
return Response({"error": "Max attempts exhausted. Please try again later."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Trigger the email
|
||||
magic_link.delay(email, key, token, current_site)
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
if user.is_password_autoset:
|
||||
# send email
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
)
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
# User should enter password to login
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import uuid
|
|||
import random
|
||||
import string
|
||||
import json
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
|
@ -20,7 +18,7 @@ from rest_framework.permissions import AllowAny
|
|||
from rest_framework import status
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from sentry_sdk import capture_exception, capture_message
|
||||
from sentry_sdk import capture_message
|
||||
|
||||
# Module imports
|
||||
from . import BaseAPIView
|
||||
|
|
@ -32,10 +30,12 @@ from plane.db.models import (
|
|||
ProjectMember,
|
||||
)
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
||||
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
refresh = RefreshToken.for_user(user)
|
||||
|
|
@ -50,22 +50,6 @@ class SignUpEndpoint(BaseAPIView):
|
|||
|
||||
def post(self, request):
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
if (
|
||||
not get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
email = request.data.get("email", False)
|
||||
password = request.data.get("password", False)
|
||||
|
|
@ -87,6 +71,24 @@ class SignUpEndpoint(BaseAPIView):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
== "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=email,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if the user already exists
|
||||
if User.objects.filter(email=email).exists():
|
||||
return Response(
|
||||
|
|
@ -105,81 +107,16 @@ class SignUpEndpoint(BaseAPIView):
|
|||
user.token_updated_at = timezone.now()
|
||||
user.save()
|
||||
|
||||
# Check if user has any accepted invites for workspace and add them to workspace
|
||||
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||
email=user.email, accepted=True
|
||||
)
|
||||
|
||||
WorkspaceMember.objects.bulk_create(
|
||||
[
|
||||
WorkspaceMember(
|
||||
workspace_id=workspace_member_invite.workspace_id,
|
||||
member=user,
|
||||
role=workspace_member_invite.role,
|
||||
)
|
||||
for workspace_member_invite in workspace_member_invites
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Check if user has any project invites
|
||||
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||
email=user.email, accepted=True
|
||||
)
|
||||
|
||||
# Add user to workspace
|
||||
WorkspaceMember.objects.bulk_create(
|
||||
[
|
||||
WorkspaceMember(
|
||||
workspace_id=project_member_invite.workspace_id,
|
||||
role=project_member_invite.role
|
||||
if project_member_invite.role in [5, 10, 15]
|
||||
else 15,
|
||||
member=user,
|
||||
created_by_id=project_member_invite.created_by_id,
|
||||
)
|
||||
for project_member_invite in project_member_invites
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Now add the users to project
|
||||
ProjectMember.objects.bulk_create(
|
||||
[
|
||||
ProjectMember(
|
||||
workspace_id=project_member_invite.workspace_id,
|
||||
role=project_member_invite.role
|
||||
if project_member_invite.role in [5, 10, 15]
|
||||
else 15,
|
||||
member=user,
|
||||
created_by_id=project_member_invite.created_by_id,
|
||||
)
|
||||
for project_member_invite in project_member_invites
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
# Delete all the invites
|
||||
workspace_member_invites.delete()
|
||||
project_member_invites.delete()
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=True
|
||||
)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
|
||||
data = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
|
||||
# Update instance user count
|
||||
update_user_instance_user_count.delay()
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
|
@ -207,8 +144,18 @@ class SignInEndpoint(BaseAPIView):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if the instance setup is done or not
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
return Response(
|
||||
{"error": "Instance is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the user
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# User is not present in db
|
||||
if user is None:
|
||||
return Response(
|
||||
{
|
||||
|
|
@ -217,7 +164,7 @@ class SignInEndpoint(BaseAPIView):
|
|||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Sign up Process
|
||||
# Check user password
|
||||
if not user.check_password(password):
|
||||
return Response(
|
||||
{
|
||||
|
|
@ -292,7 +239,7 @@ class SignInEndpoint(BaseAPIView):
|
|||
# Delete all the invites
|
||||
workspace_member_invites.delete()
|
||||
project_member_invites.delete()
|
||||
# Send event
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
|
|
@ -301,7 +248,7 @@ class SignInEndpoint(BaseAPIView):
|
|||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=False
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
|
|
@ -335,101 +282,19 @@ class SignOutEndpoint(BaseAPIView):
|
|||
return Response({"message": "success"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class MagicSignInGenerateEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
email = request.data.get("email", False)
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
if (
|
||||
not get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
||||
)
|
||||
and not (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
)
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not email:
|
||||
return Response(
|
||||
{"error": "Please provide a valid email address"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Clean up
|
||||
email = email.strip().lower()
|
||||
validate_email(email)
|
||||
|
||||
## Generate a random token
|
||||
token = (
|
||||
"".join(random.choices(string.ascii_lowercase, k=4))
|
||||
+ "-"
|
||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
||||
+ "-"
|
||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
||||
)
|
||||
|
||||
ri = redis_instance()
|
||||
|
||||
key = "magic_" + str(email)
|
||||
|
||||
# Check if the key already exists in python
|
||||
if ri.exists(key):
|
||||
data = json.loads(ri.get(key))
|
||||
|
||||
current_attempt = data["current_attempt"] + 1
|
||||
|
||||
if data["current_attempt"] > 2:
|
||||
return Response(
|
||||
{"error": "Max attempts exhausted. Please try again later."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
value = {
|
||||
"current_attempt": current_attempt,
|
||||
"email": email,
|
||||
"token": token,
|
||||
}
|
||||
expiry = 600
|
||||
|
||||
ri.set(key, json.dumps(value), ex=expiry)
|
||||
|
||||
else:
|
||||
value = {"current_attempt": 0, "email": email, "token": token}
|
||||
expiry = 600
|
||||
|
||||
ri.set(key, json.dumps(value), ex=expiry)
|
||||
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
magic_link.delay(email, key, token, current_site)
|
||||
|
||||
return Response({"key": key}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class MagicSignInEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
return Response(
|
||||
{"error": "Instance is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
user_token = request.data.get("token", "").strip()
|
||||
key = request.data.get("key", False).strip().lower()
|
||||
|
||||
|
|
@ -448,48 +313,28 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||
email = data["email"]
|
||||
|
||||
if str(token) == str(user_token):
|
||||
if User.objects.filter(email=email).exists():
|
||||
user = User.objects.get(email=email)
|
||||
if not user.is_active:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=False
|
||||
)
|
||||
|
||||
else:
|
||||
user = User.objects.create(
|
||||
user = User.objects.get(email=email)
|
||||
if not user.is_active:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
username=uuid.uuid4().hex,
|
||||
password=make_password(uuid.uuid4().hex),
|
||||
is_password_autoset=True,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=True
|
||||
)
|
||||
|
||||
user.is_active = True
|
||||
user.is_email_verified = True
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import uuid
|
||||
import requests
|
||||
import os
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
|
@ -31,8 +30,9 @@ from plane.db.models import (
|
|||
)
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
from .base import BaseAPIView
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
||||
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
|
|
@ -136,6 +136,14 @@ class OauthEndpoint(BaseAPIView):
|
|||
|
||||
def post(self, request):
|
||||
try:
|
||||
# Check if instance is registered or not
|
||||
instance = Instance.objects.first()
|
||||
if instance is None and not instance.is_setup_done:
|
||||
return Response(
|
||||
{"error": "Instance is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
medium = request.data.get("medium", False)
|
||||
id_token = request.data.get("credential", False)
|
||||
client_id = request.data.get("clientId", False)
|
||||
|
|
@ -143,34 +151,17 @@ class OauthEndpoint(BaseAPIView):
|
|||
instance_configuration = InstanceConfiguration.objects.values(
|
||||
"key", "value"
|
||||
)
|
||||
if (
|
||||
(
|
||||
not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GOOGLE_CLIENT_ID",
|
||||
os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
)
|
||||
or not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GITHUB_CLIENT_ID",
|
||||
os.environ.get("GITHUB_CLIENT_ID"),
|
||||
)
|
||||
)
|
||||
and not (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
)
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
).exists()
|
||||
if not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GOOGLE_CLIENT_ID",
|
||||
os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
) or not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GITHUB_CLIENT_ID",
|
||||
os.environ.get("GITHUB_CLIENT_ID"),
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
},
|
||||
{"error": "Github or Google login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
|
@ -286,8 +277,8 @@ class OauthEndpoint(BaseAPIView):
|
|||
"last_login_at": timezone.now(),
|
||||
},
|
||||
)
|
||||
|
||||
# Send event
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
|
|
@ -295,8 +286,8 @@ class OauthEndpoint(BaseAPIView):
|
|||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium=medium.upper(),
|
||||
first_time=False
|
||||
medium=medium.upper(),
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
|
|
@ -309,6 +300,16 @@ class OauthEndpoint(BaseAPIView):
|
|||
|
||||
except User.DoesNotExist:
|
||||
## Signup Case
|
||||
instance_configuration = InstanceConfiguration.objects.values(
|
||||
"key", "value"
|
||||
)
|
||||
# Check if instance is registered or not
|
||||
instance = Instance.objects.first()
|
||||
if instance is None and not instance.is_setup_done:
|
||||
return Response(
|
||||
{"error": "Instance is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if (
|
||||
get_configuration_value(
|
||||
|
|
@ -316,8 +317,9 @@ class OauthEndpoint(BaseAPIView):
|
|||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
== "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
email=email,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
|
|
@ -341,7 +343,7 @@ class OauthEndpoint(BaseAPIView):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
user = User(
|
||||
user = User.objects.create(
|
||||
username=username,
|
||||
email=email,
|
||||
mobile_number=mobile_number,
|
||||
|
|
@ -352,7 +354,6 @@ class OauthEndpoint(BaseAPIView):
|
|||
)
|
||||
|
||||
user.set_password(uuid.uuid4().hex)
|
||||
user.is_password_autoset = True
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
|
|
@ -418,7 +419,7 @@ class OauthEndpoint(BaseAPIView):
|
|||
workspace_member_invites.delete()
|
||||
project_member_invites.delete()
|
||||
|
||||
# Send event
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
|
|
@ -427,7 +428,7 @@ class OauthEndpoint(BaseAPIView):
|
|||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium=medium.upper(),
|
||||
first_time=True
|
||||
first_time=True,
|
||||
)
|
||||
|
||||
SocialLoginConnection.objects.update_or_create(
|
||||
|
|
@ -445,4 +446,7 @@ class OauthEndpoint(BaseAPIView):
|
|||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
|
||||
# Update the user count
|
||||
update_user_instance_user_count.delay()
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from plane.license.models import Instance, InstanceAdmin
|
|||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
|
||||
from django.db.models import Q, F, Count, Case, When, Value, IntegerField
|
||||
from django.db.models import Q, F, Count, Case, When, IntegerField
|
||||
|
||||
|
||||
class UserEndpoint(BaseViewSet):
|
||||
|
|
@ -52,7 +52,6 @@ class UserEndpoint(BaseViewSet):
|
|||
projects_to_deactivate = []
|
||||
workspaces_to_deactivate = []
|
||||
|
||||
|
||||
projects = ProjectMember.objects.filter(
|
||||
member=request.user, is_active=True
|
||||
).annotate(
|
||||
|
|
@ -155,3 +154,4 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
|||
issue_activities, many=True
|
||||
).data,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue