chore: updated sign-in workflows for cloud and self-hosted instances (#2994)
* chore: update onboarding workflow * dev: update user count tasks * fix: forgot password endpoint * dev: instance and onboarding updates * chore: update sign-in workflow for cloud and self-hosted instances (#2993) * chore: updated auth services * chore: new signin workflow updated * chore: updated content * chore: instance admin setup * dev: update instance verification task * dev: run the instance verification task every 4 hours * dev: update migrations * chore: update latest features image --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
parent
f481957818
commit
be2cf2e842
53 changed files with 1017 additions and 1368 deletions
|
|
@ -87,6 +87,7 @@ from .auth_extended import (
|
|||
ChangePasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
EmailCheckEndpoint,
|
||||
MagicGenerateEndpoint,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ 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 (
|
||||
|
|
@ -108,13 +108,16 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
return Response({"error": "Please enter a valid email"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"error": "Please enter a valid email"},
|
||||
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 = get_tokens_for_user(user=user)
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
# send the forgot password email
|
||||
forgot_password.delay(
|
||||
|
|
@ -130,7 +133,9 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class ResetPasswordEndpoint(BaseAPIView):
|
||||
permission_classes = [AllowAny,]
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request, uidb64, token):
|
||||
try:
|
||||
|
|
@ -219,6 +224,89 @@ class SetUserPasswordEndpoint(BaseAPIView):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class MagicGenerateEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Check the instance registration
|
||||
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,
|
||||
)
|
||||
|
||||
if not email:
|
||||
return Response(
|
||||
{"error": "Please provide a valid email address"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Clean up the email
|
||||
email = email.strip().lower()
|
||||
validate_email(email)
|
||||
|
||||
# check if the email exists not
|
||||
if not User.objects.filter(email=email).exists():
|
||||
# Create a user
|
||||
_ = User.objects.create(
|
||||
email=email,
|
||||
username=uuid.uuid4().hex,
|
||||
password=make_password(uuid.uuid4().hex),
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
||||
## 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)
|
||||
|
||||
# If the smtp is configured send through here
|
||||
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 EmailCheckEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
|
|
@ -237,16 +325,19 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||
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)
|
||||
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)
|
||||
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()
|
||||
|
|
@ -281,71 +372,59 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||
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(
|
||||
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
|
||||
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="EMAIL",
|
||||
first_time=True,
|
||||
)
|
||||
# Automatically send the email
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
|
||||
# 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, "is_existing": False},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
# Existing user
|
||||
else:
|
||||
if type == "magic_code":
|
||||
if user.is_password_autoset:
|
||||
## Generate a random token
|
||||
if not bool(get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN")),
|
||||
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,
|
||||
|
|
@ -356,15 +435,24 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||
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)
|
||||
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)
|
||||
return Response(
|
||||
{
|
||||
"is_password_autoset": user.is_password_autoset,
|
||||
"is_existing": True,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
else:
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
|
|
@ -376,14 +464,12 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||
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)
|
||||
|
||||
# User should enter password to login
|
||||
return Response(
|
||||
{
|
||||
"is_password_autoset": user.is_password_autoset,
|
||||
"is_existing": True,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Python imports
|
||||
import os
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
|
|
@ -10,6 +8,7 @@ from django.utils import timezone
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
|
|
@ -31,7 +30,6 @@ from plane.settings.redis import redis_instance
|
|||
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.user_count_task import update_user_instance_user_count
|
||||
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
|
|
@ -58,7 +56,6 @@ class SignUpEndpoint(BaseAPIView):
|
|||
|
||||
email = request.data.get("email", False)
|
||||
password = request.data.get("password", False)
|
||||
|
||||
## Raise exception if any of the above are missing
|
||||
if not email or not password:
|
||||
return Response(
|
||||
|
|
@ -66,8 +63,8 @@ class SignUpEndpoint(BaseAPIView):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate the email
|
||||
email = email.strip().lower()
|
||||
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError as e:
|
||||
|
|
@ -106,6 +103,7 @@ class SignUpEndpoint(BaseAPIView):
|
|||
user.set_password(password)
|
||||
|
||||
# settings last actives for the user
|
||||
user.is_password_autoset = False
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
|
|
@ -120,9 +118,6 @@ class SignUpEndpoint(BaseAPIView):
|
|||
"refresh_token": refresh_token,
|
||||
}
|
||||
|
||||
# Update instance user count
|
||||
update_user_instance_user_count.delay()
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
|
@ -148,8 +143,8 @@ class SignInEndpoint(BaseAPIView):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate email
|
||||
email = email.strip().lower()
|
||||
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError as e:
|
||||
|
|
@ -161,22 +156,45 @@ class SignInEndpoint(BaseAPIView):
|
|||
# Get the user
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# User is not present in db
|
||||
if user is None:
|
||||
return Response(
|
||||
{
|
||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# Existing user
|
||||
if user:
|
||||
# Check user password
|
||||
if not user.check_password(password):
|
||||
return Response(
|
||||
{
|
||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Check user password
|
||||
if not user.check_password(password):
|
||||
return Response(
|
||||
{
|
||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
# Create the user
|
||||
else:
|
||||
# Get the configurations
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
# 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(password),
|
||||
is_password_autoset=False,
|
||||
)
|
||||
|
||||
# settings last active for the user
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from rest_framework.response import Response
|
|||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.license.models import Instance, InstanceConfiguration
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
|
|
@ -104,4 +104,6 @@ class ConfigurationEndpoint(BaseAPIView):
|
|||
|
||||
data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||
|
||||
data["is_self_managed"] = bool(int(os.environ.get("IS_SELF_MANAGED", "1")))
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ from plane.bgtasks.event_tracking_task import auth_events
|
|||
from .base import BaseAPIView
|
||||
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):
|
||||
|
|
@ -439,6 +438,4 @@ class OauthEndpoint(BaseAPIView):
|
|||
"refresh_token": refresh_token,
|
||||
}
|
||||
|
||||
# Update the user count
|
||||
update_user_instance_user_count.delay()
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue