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:
Aaryan Khandelwal 2023-12-06 14:22:59 +05:30 committed by sriram veeraghanta
parent f481957818
commit be2cf2e842
53 changed files with 1017 additions and 1368 deletions

View file

@ -87,6 +87,7 @@ from .auth_extended import (
ChangePasswordEndpoint,
SetUserPasswordEndpoint,
EmailCheckEndpoint,
MagicGenerateEndpoint,
)

View file

@ -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,
)

View file

@ -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

View file

@ -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)

View file

@ -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)