forked from github/plane
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
b957e4e935
commit
11987994a1
@ -7,6 +7,7 @@ from plane.app.views import (
|
|||||||
# Authentication
|
# Authentication
|
||||||
SignInEndpoint,
|
SignInEndpoint,
|
||||||
SignOutEndpoint,
|
SignOutEndpoint,
|
||||||
|
MagicGenerateEndpoint,
|
||||||
MagicSignInEndpoint,
|
MagicSignInEndpoint,
|
||||||
OauthEndpoint,
|
OauthEndpoint,
|
||||||
EmailCheckEndpoint,
|
EmailCheckEndpoint,
|
||||||
@ -30,6 +31,7 @@ urlpatterns = [
|
|||||||
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||||
# magic sign in
|
# magic sign in
|
||||||
|
path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"),
|
||||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
# Password Manipulation
|
# Password Manipulation
|
||||||
|
@ -87,6 +87,7 @@ from .auth_extended import (
|
|||||||
ChangePasswordEndpoint,
|
ChangePasswordEndpoint,
|
||||||
SetUserPasswordEndpoint,
|
SetUserPasswordEndpoint,
|
||||||
EmailCheckEndpoint,
|
EmailCheckEndpoint,
|
||||||
|
MagicGenerateEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,9 +37,9 @@ from plane.bgtasks.forgot_password_task import forgot_password
|
|||||||
from plane.license.models import Instance, InstanceConfiguration
|
from plane.license.models import Instance, InstanceConfiguration
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
from plane.bgtasks.magic_link_code_task import magic_link
|
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
|
from plane.bgtasks.event_tracking_task import auth_events
|
||||||
|
|
||||||
|
|
||||||
def get_tokens_for_user(user):
|
def get_tokens_for_user(user):
|
||||||
refresh = RefreshToken.for_user(user)
|
refresh = RefreshToken.for_user(user)
|
||||||
return (
|
return (
|
||||||
@ -108,13 +108,16 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||||||
try:
|
try:
|
||||||
validate_email(email)
|
validate_email(email)
|
||||||
except ValidationError:
|
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
|
# Get the user
|
||||||
user = User.objects.filter(email=email).first()
|
user = User.objects.filter(email=email).first()
|
||||||
if user:
|
if user:
|
||||||
# Get the reset token for 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")
|
current_site = request.META.get("HTTP_ORIGIN")
|
||||||
# send the forgot password email
|
# send the forgot password email
|
||||||
forgot_password.delay(
|
forgot_password.delay(
|
||||||
@ -130,7 +133,9 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class ResetPasswordEndpoint(BaseAPIView):
|
class ResetPasswordEndpoint(BaseAPIView):
|
||||||
permission_classes = [AllowAny,]
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
def post(self, request, uidb64, token):
|
def post(self, request, uidb64, token):
|
||||||
try:
|
try:
|
||||||
@ -219,6 +224,89 @@ class SetUserPasswordEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
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):
|
class EmailCheckEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
@ -237,16 +325,19 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||||
|
|
||||||
email = request.data.get("email", False)
|
email = request.data.get("email", False)
|
||||||
type = request.data.get("type", "magic_code")
|
|
||||||
|
|
||||||
if not email:
|
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
|
# validate the email
|
||||||
try:
|
try:
|
||||||
validate_email(email)
|
validate_email(email)
|
||||||
except ValidationError:
|
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
|
# Check if the user exists
|
||||||
user = User.objects.filter(email=email).first()
|
user = User.objects.filter(email=email).first()
|
||||||
@ -281,71 +372,59 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
is_password_autoset=True,
|
is_password_autoset=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update instance user count
|
|
||||||
update_user_instance_user_count.delay()
|
|
||||||
|
|
||||||
# Case when the user selects magic code
|
if not bool(
|
||||||
if type == "magic_code":
|
get_configuration_value(
|
||||||
if not bool(get_configuration_value(
|
|
||||||
instance_configuration,
|
instance_configuration,
|
||||||
"ENABLE_MAGIC_LINK_LOGIN",
|
"ENABLE_MAGIC_LINK_LOGIN",
|
||||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN")),
|
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
||||||
):
|
),
|
||||||
return Response(
|
):
|
||||||
{"error": "Magic link sign in is disabled."},
|
return Response(
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
{"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:
|
# Send event
|
||||||
auth_events.delay(
|
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||||
user=user.id,
|
auth_events.delay(
|
||||||
email=email,
|
user=user.id,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
email=email,
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
event_name="SIGN_IN",
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
medium="EMAIL",
|
event_name="SIGN_IN",
|
||||||
first_time=True,
|
medium="MAGIC_LINK",
|
||||||
)
|
first_time=True,
|
||||||
# Automatically send the email
|
)
|
||||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
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
|
# Existing user
|
||||||
else:
|
else:
|
||||||
if type == "magic_code":
|
if user.is_password_autoset:
|
||||||
## Generate a random token
|
## Generate a random token
|
||||||
if not bool(get_configuration_value(
|
if not bool(
|
||||||
instance_configuration,
|
get_configuration_value(
|
||||||
"ENABLE_MAGIC_LINK_LOGIN",
|
instance_configuration,
|
||||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN")),
|
"ENABLE_MAGIC_LINK_LOGIN",
|
||||||
|
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
||||||
|
),
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Magic link sign in is disabled."},
|
{"error": "Magic link sign in is disabled."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||||
auth_events.delay(
|
auth_events.delay(
|
||||||
user=user.id,
|
user=user.id,
|
||||||
@ -356,15 +435,24 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
medium="MAGIC_LINK",
|
medium="MAGIC_LINK",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate magic token
|
# Generate magic token
|
||||||
key, token, current_attempt = generate_magic_token(email=email)
|
key, token, current_attempt = generate_magic_token(email=email)
|
||||||
if not current_attempt:
|
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
|
# Trigger the email
|
||||||
magic_link.delay(email, key, token, current_site)
|
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:
|
else:
|
||||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||||
auth_events.delay(
|
auth_events.delay(
|
||||||
@ -376,14 +464,12 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
medium="EMAIL",
|
medium="EMAIL",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if user.is_password_autoset:
|
# User should enter password to login
|
||||||
# send email
|
return Response(
|
||||||
uidb64, token = generate_password_token(user=user)
|
{
|
||||||
forgot_password.delay(
|
"is_password_autoset": user.is_password_autoset,
|
||||||
user.first_name, user.email, uidb64, token, current_site
|
"is_existing": True,
|
||||||
)
|
},
|
||||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
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)
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
@ -10,6 +8,7 @@ from django.utils import timezone
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
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.models import InstanceConfiguration, Instance
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
from plane.bgtasks.event_tracking_task import auth_events
|
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):
|
def get_tokens_for_user(user):
|
||||||
@ -58,7 +56,6 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
email = request.data.get("email", False)
|
email = request.data.get("email", False)
|
||||||
password = request.data.get("password", False)
|
password = request.data.get("password", False)
|
||||||
|
|
||||||
## Raise exception if any of the above are missing
|
## Raise exception if any of the above are missing
|
||||||
if not email or not password:
|
if not email or not password:
|
||||||
return Response(
|
return Response(
|
||||||
@ -66,8 +63,8 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate the email
|
||||||
email = email.strip().lower()
|
email = email.strip().lower()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validate_email(email)
|
validate_email(email)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
@ -106,6 +103,7 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
|
|
||||||
# settings last actives for the user
|
# settings last actives for the user
|
||||||
|
user.is_password_autoset = False
|
||||||
user.last_active = timezone.now()
|
user.last_active = timezone.now()
|
||||||
user.last_login_time = timezone.now()
|
user.last_login_time = timezone.now()
|
||||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||||
@ -120,9 +118,6 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Update instance user count
|
|
||||||
update_user_instance_user_count.delay()
|
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@ -148,8 +143,8 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate email
|
||||||
email = email.strip().lower()
|
email = email.strip().lower()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validate_email(email)
|
validate_email(email)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
@ -161,22 +156,45 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
# Get the user
|
# Get the user
|
||||||
user = User.objects.filter(email=email).first()
|
user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
# User is not present in db
|
# Existing user
|
||||||
if user is None:
|
if user:
|
||||||
return Response(
|
# Check user password
|
||||||
{
|
if not user.check_password(password):
|
||||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
return Response(
|
||||||
},
|
{
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||||
)
|
},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
# Check user password
|
# Create the user
|
||||||
if not user.check_password(password):
|
else:
|
||||||
return Response(
|
# Get the configurations
|
||||||
{
|
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
# Create the user
|
||||||
},
|
if (
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
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
|
# settings last active for the user
|
||||||
|
@ -11,7 +11,7 @@ from rest_framework.response import Response
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView
|
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
|
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["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)
|
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 .base import BaseAPIView
|
||||||
from plane.license.models import InstanceConfiguration, Instance
|
from plane.license.models import InstanceConfiguration, Instance
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
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):
|
def get_tokens_for_user(user):
|
||||||
@ -439,6 +438,4 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Update the user count
|
|
||||||
update_user_instance_user_count.delay()
|
|
||||||
return Response(data, status=status.HTTP_201_CREATED)
|
return Response(data, status=status.HTTP_201_CREATED)
|
||||||
|
@ -66,30 +66,6 @@ def send_export_email(email, slug, csv_buffer, rows):
|
|||||||
EMAIL_FROM,
|
EMAIL_FROM,
|
||||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||||
|
|
||||||
# Send the email if the users don't have smtp configured
|
|
||||||
if EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD:
|
|
||||||
# Check the instance registration
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-instance-id": instance.instance_id,
|
|
||||||
"x-api-key": instance.api_key,
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"email": email,
|
|
||||||
"slug": slug,
|
|
||||||
"rows": rows,
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = requests.post(
|
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/analytics/",
|
|
||||||
headers=headers,
|
|
||||||
json=payload,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
connection = get_connection(
|
connection = get_connection(
|
||||||
host=EMAIL_HOST,
|
host=EMAIL_HOST,
|
||||||
port=int(EMAIL_PORT),
|
port=int(EMAIL_PORT),
|
||||||
|
@ -39,32 +39,6 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
|||||||
EMAIL_FROM,
|
EMAIL_FROM,
|
||||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||||
|
|
||||||
# Send the email if the users don't have smtp configured
|
|
||||||
if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
|
|
||||||
# Check the instance registration
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
|
|
||||||
# headers
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-instance-id": instance.instance_id,
|
|
||||||
"x-api-key": instance.api_key,
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"abs_url": abs_url,
|
|
||||||
"first_name": first_name,
|
|
||||||
"email": email,
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = requests.post(
|
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/forgot-password/",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps(payload),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
subject = "A new password to your Plane account has been requested"
|
subject = "A new password to your Plane account has been requested"
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
@ -26,7 +26,6 @@ from plane.db.models import (
|
|||||||
IssueProperty,
|
IssueProperty,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.user_welcome_task import send_welcome_slack
|
from plane.bgtasks.user_welcome_task import send_welcome_slack
|
||||||
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
@ -121,9 +120,6 @@ def service_importer(service, importer_id):
|
|||||||
batch_size=100,
|
batch_size=100,
|
||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update instance user count
|
|
||||||
update_user_instance_user_count.delay()
|
|
||||||
|
|
||||||
# Check if sync config is on for github importers
|
# Check if sync config is on for github importers
|
||||||
if service == "github" and importer.config.get("sync", False):
|
if service == "github" and importer.config.get("sync", False):
|
||||||
|
@ -34,30 +34,6 @@ def magic_link(email, key, token, current_site):
|
|||||||
EMAIL_FROM,
|
EMAIL_FROM,
|
||||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||||
|
|
||||||
# Send the email if the users don't have smtp configured
|
|
||||||
if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
|
|
||||||
# Check the instance registration
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-instance-id": instance.instance_id,
|
|
||||||
"x-api-key": instance.api_key,
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"token": token,
|
|
||||||
"email": email,
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = requests.post(
|
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/magic-code/",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps(payload),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
# Send the mail
|
# Send the mail
|
||||||
subject = f"Your unique Plane login code is {token}"
|
subject = f"Your unique Plane login code is {token}"
|
||||||
context = {"code": token, "email": email}
|
context = {"code": token, "email": email}
|
||||||
|
@ -14,7 +14,7 @@ from sentry_sdk import capture_exception
|
|||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import Project, User, ProjectMemberInvite
|
from plane.db.models import Project, User, ProjectMemberInvite
|
||||||
from plane.license.models import InstanceConfiguration
|
from plane.license.models import InstanceConfiguration
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
from plane.license.utils.instance_value import get_email_configuration
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def project_invitation(email, project_id, token, current_site, invitor):
|
def project_invitation(email, project_id, token, current_site, invitor):
|
||||||
@ -48,42 +48,27 @@ def project_invitation(email, project_id, token, current_site, invitor):
|
|||||||
|
|
||||||
# Configure email connection from the database
|
# Configure email connection from the database
|
||||||
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||||
|
(
|
||||||
|
EMAIL_HOST,
|
||||||
|
EMAIL_HOST_USER,
|
||||||
|
EMAIL_HOST_PASSWORD,
|
||||||
|
EMAIL_PORT,
|
||||||
|
EMAIL_USE_TLS,
|
||||||
|
EMAIL_FROM,
|
||||||
|
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||||
|
|
||||||
connection = get_connection(
|
connection = get_connection(
|
||||||
host=get_configuration_value(
|
host=EMAIL_HOST,
|
||||||
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST")
|
port=int(EMAIL_PORT),
|
||||||
),
|
username=EMAIL_HOST_USER,
|
||||||
port=int(
|
password=EMAIL_HOST_PASSWORD,
|
||||||
get_configuration_value(
|
use_tls=bool(EMAIL_USE_TLS),
|
||||||
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
username=get_configuration_value(
|
|
||||||
instance_configuration,
|
|
||||||
"EMAIL_HOST_USER",
|
|
||||||
os.environ.get("EMAIL_HOST_USER"),
|
|
||||||
),
|
|
||||||
password=get_configuration_value(
|
|
||||||
instance_configuration,
|
|
||||||
"EMAIL_HOST_PASSWORD",
|
|
||||||
os.environ.get("EMAIL_HOST_PASSWORD"),
|
|
||||||
),
|
|
||||||
use_tls=bool(
|
|
||||||
get_configuration_value(
|
|
||||||
instance_configuration,
|
|
||||||
"EMAIL_USE_TLS",
|
|
||||||
os.environ.get("EMAIL_USE_TLS", "1"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(
|
msg = EmailMultiAlternatives(
|
||||||
subject=subject,
|
subject=subject,
|
||||||
body=text_content,
|
body=text_content,
|
||||||
from_email=get_configuration_value(
|
from_email=EMAIL_FROM,
|
||||||
instance_configuration,
|
|
||||||
"EMAIL_FROM",
|
|
||||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
|
||||||
),
|
|
||||||
to=[email],
|
to=[email],
|
||||||
connection=connection,
|
connection=connection,
|
||||||
)
|
)
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import os
|
|
||||||
|
|
||||||
# django imports
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
# Third party imports
|
|
||||||
from celery import shared_task
|
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
# Module imports
|
|
||||||
from plane.db.models import User
|
|
||||||
from plane.license.models import Instance
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def update_user_instance_user_count():
|
|
||||||
try:
|
|
||||||
instance_users = User.objects.filter(is_bot=False).count()
|
|
||||||
instance = Instance.objects.update(user_count=instance_users)
|
|
||||||
|
|
||||||
# Update the count in the license engine
|
|
||||||
payload = {
|
|
||||||
"user_count": User.objects.count(),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Save the user in control center
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-instance-id": instance.instance_id,
|
|
||||||
"x-api-key": instance.api_key,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update the license engine
|
|
||||||
_ = requests.post(
|
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps(payload),
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if settings.DEBUG:
|
|
||||||
print(e)
|
|
||||||
capture_exception(e)
|
|
@ -50,31 +50,6 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
|||||||
EMAIL_FROM,
|
EMAIL_FROM,
|
||||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||||
|
|
||||||
# Send the email if the users don't have smtp configured
|
|
||||||
if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
|
|
||||||
# Check the instance registration
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-instance-id": instance.instance_id,
|
|
||||||
"x-api-key": instance.api_key,
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"user": user.first_name or user.display_name or user.email,
|
|
||||||
"workspace_name": workspace.name,
|
|
||||||
"invitation_url": abs_url,
|
|
||||||
"email": email,
|
|
||||||
}
|
|
||||||
_ = requests.post(
|
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/workspace-invitation/",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps(payload),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
# Subject of the email
|
# Subject of the email
|
||||||
subject = f"{user.first_name or user.display_name or user.email} has invited you to join them in {workspace.name} on Plane"
|
subject = f"{user.first_name or user.display_name or user.email} has invited you to join them in {workspace.name} on Plane"
|
||||||
|
|
||||||
|
@ -28,9 +28,13 @@ app.conf.beat_schedule = {
|
|||||||
"task": "plane.bgtasks.file_asset_task.delete_file_asset",
|
"task": "plane.bgtasks.file_asset_task.delete_file_asset",
|
||||||
"schedule": crontab(hour=0, minute=0),
|
"schedule": crontab(hour=0, minute=0),
|
||||||
},
|
},
|
||||||
|
"check-instance-verification": {
|
||||||
|
"task": "plane.license.bgtasks.instance_verification_task.instance_verification_task",
|
||||||
|
"schedule": crontab(minute=0, hour='*/4'),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Load task modules from all registered Django app configs.
|
# Load task modules from all registered Django app configs.
|
||||||
app.autodiscover_tasks()
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler'
|
app.conf.beat_scheduler = "django_celery_beat.schedulers.DatabaseScheduler"
|
||||||
|
@ -43,7 +43,7 @@ class InstanceConfigurationSerializer(BaseSerializer):
|
|||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
# Decrypt secrets value
|
# Decrypt secrets value
|
||||||
if instance.key in ["OPENAI_API_KEY", "GITHUB_CLIENT_SECRET", "EMAIL_HOST_PASSWORD", "UNSPLASH_ACESS_KEY"] and instance.value is not None:
|
if instance.is_encrypted and instance.value is not None:
|
||||||
data["value"] = decrypt_data(instance.value)
|
data["value"] = decrypt_data(instance.value)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -2,8 +2,6 @@ from .instance import (
|
|||||||
InstanceEndpoint,
|
InstanceEndpoint,
|
||||||
InstanceAdminEndpoint,
|
InstanceAdminEndpoint,
|
||||||
InstanceConfigurationEndpoint,
|
InstanceConfigurationEndpoint,
|
||||||
AdminSetupMagicSignInEndpoint,
|
InstanceAdminSignInEndpoint,
|
||||||
SignUpScreenVisitedEndpoint,
|
SignUpScreenVisitedEndpoint,
|
||||||
AdminMagicSignInGenerateEndpoint,
|
|
||||||
AdminSetUserPasswordEndpoint,
|
|
||||||
)
|
)
|
||||||
|
@ -27,14 +27,11 @@ from plane.license.api.serializers import (
|
|||||||
InstanceAdminSerializer,
|
InstanceAdminSerializer,
|
||||||
InstanceConfigurationSerializer,
|
InstanceConfigurationSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.serializers import UserSerializer
|
|
||||||
from plane.license.api.permissions import (
|
from plane.license.api.permissions import (
|
||||||
InstanceAdminPermission,
|
InstanceAdminPermission,
|
||||||
)
|
)
|
||||||
from plane.db.models import User
|
from plane.db.models import User
|
||||||
from plane.license.utils.encryption import encrypt_data
|
from plane.license.utils.encryption import encrypt_data
|
||||||
from plane.settings.redis import redis_instance
|
|
||||||
from plane.bgtasks.magic_link_code_task import magic_link
|
|
||||||
|
|
||||||
|
|
||||||
class InstanceEndpoint(BaseAPIView):
|
class InstanceEndpoint(BaseAPIView):
|
||||||
@ -47,61 +44,6 @@ class InstanceEndpoint(BaseAPIView):
|
|||||||
AllowAny(),
|
AllowAny(),
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
# Check if the instance is registered
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
|
|
||||||
# If instance is None then register this instance
|
|
||||||
if instance is None:
|
|
||||||
with open("package.json", "r") as file:
|
|
||||||
# Load JSON content from the file
|
|
||||||
data = json.load(file)
|
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"instance_key":settings.INSTANCE_KEY,
|
|
||||||
"version": data.get("version", 0.1),
|
|
||||||
"machine_signature": os.environ.get("MACHINE_SIGNATURE"),
|
|
||||||
"user_count": User.objects.filter(is_bot=False).count(),
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps(payload),
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 201:
|
|
||||||
data = response.json()
|
|
||||||
# Create instance
|
|
||||||
instance = Instance.objects.create(
|
|
||||||
instance_name="Plane Free",
|
|
||||||
instance_id=data.get("id"),
|
|
||||||
license_key=data.get("license_key"),
|
|
||||||
api_key=data.get("api_key"),
|
|
||||||
version=data.get("version"),
|
|
||||||
last_checked_at=timezone.now(),
|
|
||||||
user_count=data.get("user_count", 0),
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = InstanceSerializer(instance)
|
|
||||||
data = serializer.data
|
|
||||||
data["is_activated"] = True
|
|
||||||
return Response(
|
|
||||||
data,
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
{"error": "Instance could not be registered"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
{"message": "Instance already registered"},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
instance = Instance.objects.first()
|
instance = Instance.objects.first()
|
||||||
# get the instance
|
# get the instance
|
||||||
@ -122,24 +64,6 @@ class InstanceEndpoint(BaseAPIView):
|
|||||||
serializer = InstanceSerializer(instance, data=request.data, partial=True)
|
serializer = InstanceSerializer(instance, data=request.data, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
# Save the user in control center
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-instance-id": instance.instance_id,
|
|
||||||
"x-api-key": instance.api_key,
|
|
||||||
}
|
|
||||||
# Update instance settings in the license engine
|
|
||||||
_ = requests.patch(
|
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps(
|
|
||||||
{
|
|
||||||
"is_support_required": serializer.data["is_support_required"],
|
|
||||||
"is_telemetry_enabled": serializer.data["is_telemetry_enabled"],
|
|
||||||
"version": serializer.data["version"],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -212,12 +136,7 @@ class InstanceConfigurationEndpoint(BaseAPIView):
|
|||||||
bulk_configurations = []
|
bulk_configurations = []
|
||||||
for configuration in configurations:
|
for configuration in configurations:
|
||||||
value = request.data.get(configuration.key, configuration.value)
|
value = request.data.get(configuration.key, configuration.value)
|
||||||
if value is not None and configuration.key in [
|
if configuration.is_encrypted:
|
||||||
"OPENAI_API_KEY",
|
|
||||||
"GITHUB_CLIENT_SECRET",
|
|
||||||
"EMAIL_HOST_PASSWORD",
|
|
||||||
"UNSPLASH_ACESS_KEY",
|
|
||||||
]:
|
|
||||||
configuration.value = encrypt_data(value)
|
configuration.value = encrypt_data(value)
|
||||||
else:
|
else:
|
||||||
configuration.value = value
|
configuration.value = value
|
||||||
@ -239,15 +158,13 @@ def get_tokens_for_user(user):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdminMagicSignInGenerateEndpoint(BaseAPIView):
|
class InstanceAdminSignInEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
email = request.data.get("email", False)
|
# Check instance first
|
||||||
|
|
||||||
# Check the instance registration
|
|
||||||
instance = Instance.objects.first()
|
instance = Instance.objects.first()
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return Response(
|
return Response(
|
||||||
@ -255,193 +172,63 @@ class AdminMagicSignInGenerateEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# check if the instance is already activated
|
||||||
if InstanceAdmin.objects.first():
|
if InstanceAdmin.objects.first():
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Admin for this instance is already registered"},
|
{"error": "Admin for this instance is already registered"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not email:
|
# Get the email and password from all the user
|
||||||
return Response(
|
email = request.data.get("email", False)
|
||||||
{"error": "Please provide a valid email address"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
email = email.strip().lower()
|
|
||||||
validate_email(email)
|
|
||||||
|
|
||||||
# check if the email exists
|
|
||||||
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 AdminSetupMagicSignInEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
AllowAny,
|
|
||||||
]
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
user_token = request.data.get("token", "").strip()
|
|
||||||
key = request.data.get("key", "").strip().lower()
|
|
||||||
|
|
||||||
if not key or user_token == "":
|
|
||||||
return Response(
|
|
||||||
{"error": "User token and key are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if InstanceAdmin.objects.first():
|
|
||||||
return Response(
|
|
||||||
{"error": "Admin for this instance is already registered"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
ri = redis_instance()
|
|
||||||
|
|
||||||
if ri.exists(key):
|
|
||||||
data = json.loads(ri.get(key))
|
|
||||||
|
|
||||||
token = data["token"]
|
|
||||||
email = data["email"]
|
|
||||||
|
|
||||||
if str(token) == str(user_token):
|
|
||||||
# get the user
|
|
||||||
user = User.objects.get(email=email)
|
|
||||||
# get the email
|
|
||||||
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")
|
|
||||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
|
||||||
user.token_updated_at = timezone.now()
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
|
||||||
data = {
|
|
||||||
"access_token": access_token,
|
|
||||||
"refresh_token": refresh_token,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
{"error": "Your login code was incorrect. Please try again."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
{"error": "The magic code/link has expired please try again"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AdminSetUserPasswordEndpoint(BaseAPIView):
|
|
||||||
def post(self, request):
|
|
||||||
user = User.objects.get(pk=request.user.id)
|
|
||||||
password = request.data.get("password", False)
|
password = request.data.get("password", False)
|
||||||
|
|
||||||
# If the user password is not autoset then return error
|
# return error if the email and password is not present
|
||||||
if not user.is_password_autoset:
|
if not email or not password:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"error": "Email and password are required"},
|
||||||
"error": "Your password is already set please change your password from profile"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check password validation
|
# Validate the email
|
||||||
if not password and len(str(password)) < 8:
|
email = email.strip().lower()
|
||||||
|
try:
|
||||||
|
validate_email(email)
|
||||||
|
except ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Password is not valid"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Please provide a valid email address."},
|
||||||
)
|
|
||||||
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
if instance is None:
|
|
||||||
return Response(
|
|
||||||
{"error": "Instance is not configured"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save the user in control center
|
# Check if already a user exists or not
|
||||||
headers = {
|
user = User.objects.filter(email=email).first()
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-instance-id": instance.instance_id,
|
|
||||||
"x-api-key": instance.api_key,
|
|
||||||
}
|
|
||||||
_ = requests.patch(
|
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps({"is_setup_done": True}),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Also register the user as admin
|
# Existing user
|
||||||
_ = requests.post(
|
if user:
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/register/",
|
# Check user password
|
||||||
headers=headers,
|
if not user.check_password(password):
|
||||||
data=json.dumps(
|
return Response(
|
||||||
{
|
{
|
||||||
"email": str(user.email),
|
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||||
"signup_mode": "MAGIC_CODE",
|
},
|
||||||
"is_admin": True,
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
}
|
)
|
||||||
),
|
else:
|
||||||
)
|
user = User.objects.create(
|
||||||
|
email=email,
|
||||||
|
username=uuid.uuid4().hex,
|
||||||
|
password=make_password(password),
|
||||||
|
is_password_autoset=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# settings last active for the user
|
||||||
|
user.is_active = True
|
||||||
|
user.last_active = timezone.now()
|
||||||
|
user.last_login_time = timezone.now()
|
||||||
|
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||||
|
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||||
|
user.token_updated_at = timezone.now()
|
||||||
|
user.save()
|
||||||
|
|
||||||
# Register the user as an instance admin
|
# Register the user as an instance admin
|
||||||
_ = InstanceAdmin.objects.create(
|
_ = InstanceAdmin.objects.create(
|
||||||
@ -452,12 +239,13 @@ class AdminSetUserPasswordEndpoint(BaseAPIView):
|
|||||||
instance.is_setup_done = True
|
instance.is_setup_done = True
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
# Set the user password
|
# get tokens for user
|
||||||
user.set_password(password)
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
user.is_password_autoset = False
|
data = {
|
||||||
user.save()
|
"access_token": access_token,
|
||||||
serializer = UserSerializer(user)
|
"refresh_token": refresh_token,
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
}
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class SignUpScreenVisitedEndpoint(BaseAPIView):
|
class SignUpScreenVisitedEndpoint(BaseAPIView):
|
||||||
@ -467,27 +255,11 @@ class SignUpScreenVisitedEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
instance = Instance.objects.first()
|
instance = Instance.objects.first()
|
||||||
|
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Instance is not configured"},
|
{"error": "Instance is not configured"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
instance.is_signup_screen_visited = True
|
||||||
if not instance.is_signup_screen_visited:
|
instance.save()
|
||||||
instance.is_signup_screen_visited = True
|
|
||||||
instance.save()
|
|
||||||
# set the headers
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-instance-id": instance.instance_id,
|
|
||||||
"x-api-key": instance.api_key,
|
|
||||||
}
|
|
||||||
# create the payload
|
|
||||||
payload = {"is_signup_screen_visited": True}
|
|
||||||
_ = requests.patch(
|
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps(payload),
|
|
||||||
)
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
0
apiserver/plane/license/bgtasks/__init__.py
Normal file
0
apiserver/plane/license/bgtasks/__init__.py
Normal file
136
apiserver/plane/license/bgtasks/instance_verification_task.py
Normal file
136
apiserver/plane/license/bgtasks/instance_verification_task.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# Python imports
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import User
|
||||||
|
from plane.license.models import Instance, InstanceAdmin
|
||||||
|
|
||||||
|
|
||||||
|
def instance_verification(instance):
|
||||||
|
with open("package.json", "r") as file:
|
||||||
|
# Load JSON content from the file
|
||||||
|
data = json.load(file)
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"instance_key": settings.INSTANCE_KEY,
|
||||||
|
"version": data.get("version", 0.1),
|
||||||
|
"machine_signature": os.environ.get("MACHINE_SIGNATURE", "machine-signature"),
|
||||||
|
"user_count": User.objects.filter(is_bot=False).count(),
|
||||||
|
}
|
||||||
|
# Register the instance
|
||||||
|
response = requests.post(
|
||||||
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||||
|
headers=headers,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
# check response status
|
||||||
|
if response.status_code == 201:
|
||||||
|
data = response.json()
|
||||||
|
# Update instance
|
||||||
|
instance.instance_id = data.get("id")
|
||||||
|
instance.license_key = data.get("license_key")
|
||||||
|
instance.api_key = data.get("api_key")
|
||||||
|
instance.version = data.get("version")
|
||||||
|
instance.user_count = data.get("user_count", 0)
|
||||||
|
instance.is_verified = True
|
||||||
|
instance.save()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def admin_verification(instance):
|
||||||
|
# Save the user in control center
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-instance-id": instance.instance_id,
|
||||||
|
"x-api-key": instance.api_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all the unverified instance admins
|
||||||
|
instance_admins = InstanceAdmin.objects.filter(is_verified=False).select_related(
|
||||||
|
"user"
|
||||||
|
)
|
||||||
|
updated_instance_admin = []
|
||||||
|
|
||||||
|
# Verify the instance admin
|
||||||
|
for instance_admin in instance_admins:
|
||||||
|
instance_admin.is_verified = True
|
||||||
|
# Create the admin
|
||||||
|
response = requests.post(
|
||||||
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/register/",
|
||||||
|
headers=headers,
|
||||||
|
data=json.dumps(
|
||||||
|
{
|
||||||
|
"email": str(instance_admin.user.email),
|
||||||
|
"signup_mode": "EMAIL",
|
||||||
|
"is_admin": True,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
updated_instance_admin.append(instance_admin)
|
||||||
|
|
||||||
|
# update all the instance admins
|
||||||
|
InstanceAdmin.objects.bulk_update(
|
||||||
|
updated_instance_admin, ["is_verified"], batch_size=10
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
def instance_user_count(instance):
|
||||||
|
try:
|
||||||
|
instance_users = User.objects.filter(is_bot=False).count()
|
||||||
|
|
||||||
|
# Update the count in the license engine
|
||||||
|
payload = {
|
||||||
|
"user_count": instance_users,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save the user in control center
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-instance-id": instance.instance_id,
|
||||||
|
"x-api-key": instance.api_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the license engine
|
||||||
|
_ = requests.post(
|
||||||
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||||
|
headers=headers,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except requests.RequestException:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def instance_verification_task():
|
||||||
|
try:
|
||||||
|
# Get the first instance
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
|
||||||
|
# Only register instance if it is not verified
|
||||||
|
if not instance.is_verified:
|
||||||
|
instance_verification(instance=instance)
|
||||||
|
|
||||||
|
# Admin verifications
|
||||||
|
admin_verification(instance=instance)
|
||||||
|
|
||||||
|
# Update user count
|
||||||
|
instance_user_count(instance=instance)
|
||||||
|
|
||||||
|
return
|
||||||
|
except requests.RequestException:
|
||||||
|
return
|
@ -21,84 +21,91 @@ class Command(BaseCommand):
|
|||||||
"key": "ENABLE_SIGNUP",
|
"key": "ENABLE_SIGNUP",
|
||||||
"value": os.environ.get("ENABLE_SIGNUP", "1"),
|
"value": os.environ.get("ENABLE_SIGNUP", "1"),
|
||||||
"category": "AUTHENTICATION",
|
"category": "AUTHENTICATION",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "ENABLE_EMAIL_PASSWORD",
|
"key": "ENABLE_EMAIL_PASSWORD",
|
||||||
"value": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
"value": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
||||||
"category": "AUTHENTICATION",
|
"category": "AUTHENTICATION",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||||
"value": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"),
|
"value": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"),
|
||||||
"category": "AUTHENTICATION",
|
"category": "AUTHENTICATION",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "GOOGLE_CLIENT_ID",
|
"key": "GOOGLE_CLIENT_ID",
|
||||||
"value": os.environ.get("GOOGLE_CLIENT_ID"),
|
"value": os.environ.get("GOOGLE_CLIENT_ID"),
|
||||||
"category": "GOOGLE",
|
"category": "GOOGLE",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "GITHUB_CLIENT_ID",
|
"key": "GITHUB_CLIENT_ID",
|
||||||
"value": os.environ.get("GITHUB_CLIENT_ID"),
|
"value": os.environ.get("GITHUB_CLIENT_ID"),
|
||||||
"category": "GITHUB",
|
"category": "GITHUB",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "GITHUB_CLIENT_SECRET",
|
"key": "GITHUB_CLIENT_SECRET",
|
||||||
"value": encrypt_data(os.environ.get("GITHUB_CLIENT_SECRET"))
|
"value": os.environ.get("GITHUB_CLIENT_SECRET"),
|
||||||
if os.environ.get("GITHUB_CLIENT_SECRET")
|
|
||||||
else None,
|
|
||||||
"category": "GITHUB",
|
"category": "GITHUB",
|
||||||
|
"is_encrypted": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "EMAIL_HOST",
|
"key": "EMAIL_HOST",
|
||||||
"value": os.environ.get("EMAIL_HOST", ""),
|
"value": os.environ.get("EMAIL_HOST", ""),
|
||||||
"category": "SMTP",
|
"category": "SMTP",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "EMAIL_HOST_USER",
|
"key": "EMAIL_HOST_USER",
|
||||||
"value": os.environ.get("EMAIL_HOST_USER", ""),
|
"value": os.environ.get("EMAIL_HOST_USER", ""),
|
||||||
"category": "SMTP",
|
"category": "SMTP",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "EMAIL_HOST_PASSWORD",
|
"key": "EMAIL_HOST_PASSWORD",
|
||||||
"value": encrypt_data(os.environ.get("EMAIL_HOST_PASSWORD"))
|
"value": os.environ.get("EMAIL_HOST_PASSWORD", ""),
|
||||||
if os.environ.get("EMAIL_HOST_PASSWORD")
|
|
||||||
else None,
|
|
||||||
"category": "SMTP",
|
"category": "SMTP",
|
||||||
|
"is_encrypted": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "EMAIL_PORT",
|
"key": "EMAIL_PORT",
|
||||||
"value": os.environ.get("EMAIL_PORT", "587"),
|
"value": os.environ.get("EMAIL_PORT", "587"),
|
||||||
"category": "SMTP",
|
"category": "SMTP",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "EMAIL_FROM",
|
"key": "EMAIL_FROM",
|
||||||
"value": os.environ.get("EMAIL_FROM", ""),
|
"value": os.environ.get("EMAIL_FROM", ""),
|
||||||
"category": "SMTP",
|
"category": "SMTP",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "EMAIL_USE_TLS",
|
"key": "EMAIL_USE_TLS",
|
||||||
"value": os.environ.get("EMAIL_USE_TLS", "1"),
|
"value": os.environ.get("EMAIL_USE_TLS", "1"),
|
||||||
"category": "SMTP",
|
"category": "SMTP",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "OPENAI_API_KEY",
|
"key": "OPENAI_API_KEY",
|
||||||
"value": encrypt_data(os.environ.get("OPENAI_API_KEY"))
|
"value": os.environ.get("OPENAI_API_KEY"),
|
||||||
if os.environ.get("OPENAI_API_KEY")
|
|
||||||
else None,
|
|
||||||
"category": "OPENAI",
|
"category": "OPENAI",
|
||||||
|
"is_encrypted": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "GPT_ENGINE",
|
"key": "GPT_ENGINE",
|
||||||
"value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
|
"value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
|
||||||
"category": "SMTP",
|
"category": "SMTP",
|
||||||
|
"is_encrypted": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "UNSPLASH_ACCESS_KEY",
|
"key": "UNSPLASH_ACCESS_KEY",
|
||||||
"value": encrypt_data(os.environ.get("UNSPLASH_ACESS_KEY", ""))
|
"value": os.environ.get("UNSPLASH_ACESS_KEY", ""),
|
||||||
if os.environ.get("UNSPLASH_ACESS_KEY")
|
|
||||||
else None,
|
|
||||||
"category": "UNSPLASH",
|
"category": "UNSPLASH",
|
||||||
|
"is_encrypted": True,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -107,8 +114,12 @@ class Command(BaseCommand):
|
|||||||
key=item.get("key")
|
key=item.get("key")
|
||||||
)
|
)
|
||||||
if created:
|
if created:
|
||||||
obj.value = item.get("value")
|
|
||||||
obj.category = item.get("category")
|
obj.category = item.get("category")
|
||||||
|
obj.is_encrypted = item.get("is_encrypted", False)
|
||||||
|
if item.get("is_encrypted", False):
|
||||||
|
obj.value = encrypt_data(item.get("value"))
|
||||||
|
else:
|
||||||
|
obj.value = item.get("value")
|
||||||
obj.save()
|
obj.save()
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS(
|
self.style.SUCCESS(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import requests
|
import requests
|
||||||
|
import secrets
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
@ -31,13 +31,12 @@ class Command(BaseCommand):
|
|||||||
data = json.load(file)
|
data = json.load(file)
|
||||||
|
|
||||||
machine_signature = options.get("machine_signature", False)
|
machine_signature = options.get("machine_signature", False)
|
||||||
|
|
||||||
|
|
||||||
if not machine_signature:
|
if not machine_signature:
|
||||||
raise CommandError("Machine signature is required")
|
raise CommandError("Machine signature is required")
|
||||||
|
|
||||||
|
# Check if machine is online
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"instance_key": settings.INSTANCE_KEY,
|
"instance_key": settings.INSTANCE_KEY,
|
||||||
"version": data.get("version", 0.1),
|
"version": data.get("version", 0.1),
|
||||||
@ -45,25 +44,44 @@ class Command(BaseCommand):
|
|||||||
"user_count": User.objects.filter(is_bot=False).count(),
|
"user_count": User.objects.filter(is_bot=False).count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
response = requests.post(
|
||||||
headers=headers,
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||||
data=json.dumps(payload),
|
headers=headers,
|
||||||
)
|
data=json.dumps(payload),
|
||||||
|
timeout=30
|
||||||
if response.status_code == 201:
|
|
||||||
data = response.json()
|
|
||||||
# Create instance
|
|
||||||
instance = Instance.objects.create(
|
|
||||||
instance_name="Plane Free",
|
|
||||||
instance_id=data.get("id"),
|
|
||||||
license_key=data.get("license_key"),
|
|
||||||
api_key=data.get("api_key"),
|
|
||||||
version=data.get("version"),
|
|
||||||
last_checked_at=timezone.now(),
|
|
||||||
user_count=data.get("user_count", 0),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if response.status_code == 201:
|
||||||
|
data = response.json()
|
||||||
|
# Create instance
|
||||||
|
instance = Instance.objects.create(
|
||||||
|
instance_name="Plane Free",
|
||||||
|
instance_id=data.get("id"),
|
||||||
|
license_key=data.get("license_key"),
|
||||||
|
api_key=data.get("api_key"),
|
||||||
|
version=data.get("version"),
|
||||||
|
last_checked_at=timezone.now(),
|
||||||
|
user_count=data.get("user_count", 0),
|
||||||
|
is_verified=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Instance successfully registered and verified"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except requests.RequestException as _e:
|
||||||
|
instance = Instance.objects.create(
|
||||||
|
instance_name="Plane Free",
|
||||||
|
instance_id=secrets.token_hex(12),
|
||||||
|
license_key=None,
|
||||||
|
api_key=secrets.token_hex(8),
|
||||||
|
version=payload.get("version"),
|
||||||
|
last_checked_at=timezone.now(),
|
||||||
|
user_count=payload.get("user_count", 0),
|
||||||
|
)
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS(
|
self.style.SUCCESS(
|
||||||
f"Instance successfully registered"
|
f"Instance successfully registered"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 4.2.7 on 2023-11-29 14:39
|
# Generated by Django 4.2.7 on 2023-12-06 06:49
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -34,6 +34,7 @@ class Migration(migrations.Migration):
|
|||||||
('is_setup_done', models.BooleanField(default=False)),
|
('is_setup_done', models.BooleanField(default=False)),
|
||||||
('is_signup_screen_visited', models.BooleanField(default=False)),
|
('is_signup_screen_visited', models.BooleanField(default=False)),
|
||||||
('user_count', models.PositiveBigIntegerField(default=0)),
|
('user_count', models.PositiveBigIntegerField(default=0)),
|
||||||
|
('is_verified', models.BooleanField(default=False)),
|
||||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
],
|
],
|
||||||
@ -53,6 +54,7 @@ class Migration(migrations.Migration):
|
|||||||
('key', models.CharField(max_length=100, unique=True)),
|
('key', models.CharField(max_length=100, unique=True)),
|
||||||
('value', models.TextField(blank=True, default=None, null=True)),
|
('value', models.TextField(blank=True, default=None, null=True)),
|
||||||
('category', models.TextField()),
|
('category', models.TextField()),
|
||||||
|
('is_encrypted', models.BooleanField(default=False)),
|
||||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
],
|
],
|
||||||
@ -70,6 +72,7 @@ class Migration(migrations.Migration):
|
|||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
('role', models.PositiveIntegerField(choices=[(20, 'Admin')], default=20)),
|
('role', models.PositiveIntegerField(choices=[(20, 'Admin')], default=20)),
|
||||||
|
('is_verified', models.BooleanField(default=False)),
|
||||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||||
('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admins', to='license.instance')),
|
('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admins', to='license.instance')),
|
||||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
|
@ -30,6 +30,7 @@ class Instance(BaseModel):
|
|||||||
is_signup_screen_visited = models.BooleanField(default=False)
|
is_signup_screen_visited = models.BooleanField(default=False)
|
||||||
# users
|
# users
|
||||||
user_count = models.PositiveBigIntegerField(default=0)
|
user_count = models.PositiveBigIntegerField(default=0)
|
||||||
|
is_verified = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Instance"
|
verbose_name = "Instance"
|
||||||
@ -47,6 +48,7 @@ class InstanceAdmin(BaseModel):
|
|||||||
)
|
)
|
||||||
instance = models.ForeignKey(Instance, on_delete=models.CASCADE, related_name="admins")
|
instance = models.ForeignKey(Instance, on_delete=models.CASCADE, related_name="admins")
|
||||||
role = models.PositiveIntegerField(choices=ROLE_CHOICES, default=20)
|
role = models.PositiveIntegerField(choices=ROLE_CHOICES, default=20)
|
||||||
|
is_verified = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["instance", "user"]
|
unique_together = ["instance", "user"]
|
||||||
@ -61,6 +63,7 @@ class InstanceConfiguration(BaseModel):
|
|||||||
key = models.CharField(max_length=100, unique=True)
|
key = models.CharField(max_length=100, unique=True)
|
||||||
value = models.TextField(null=True, blank=True, default=None)
|
value = models.TextField(null=True, blank=True, default=None)
|
||||||
category = models.TextField()
|
category = models.TextField()
|
||||||
|
is_encrypted = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Instance Configuration"
|
verbose_name = "Instance Configuration"
|
||||||
|
@ -4,9 +4,7 @@ from plane.license.api.views import (
|
|||||||
InstanceEndpoint,
|
InstanceEndpoint,
|
||||||
InstanceAdminEndpoint,
|
InstanceAdminEndpoint,
|
||||||
InstanceConfigurationEndpoint,
|
InstanceConfigurationEndpoint,
|
||||||
AdminMagicSignInGenerateEndpoint,
|
InstanceAdminSignInEndpoint,
|
||||||
AdminSetupMagicSignInEndpoint,
|
|
||||||
AdminSetUserPasswordEndpoint,
|
|
||||||
SignUpScreenVisitedEndpoint,
|
SignUpScreenVisitedEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,19 +30,9 @@ urlpatterns = [
|
|||||||
name="instance-configuration",
|
name="instance-configuration",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"instances/admins/magic-generate/",
|
"instances/admins/sign-in/",
|
||||||
AdminMagicSignInGenerateEndpoint.as_view(),
|
InstanceAdminSignInEndpoint.as_view(),
|
||||||
name="instance-admins",
|
name="instance-admin-sign-in",
|
||||||
),
|
|
||||||
path(
|
|
||||||
"instances/admins/magic-sign-in/",
|
|
||||||
AdminSetupMagicSignInEndpoint.as_view(),
|
|
||||||
name="instance-admins",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"instances/admins/set-password/",
|
|
||||||
AdminSetUserPasswordEndpoint.as_view(),
|
|
||||||
name="instance-admins",
|
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"instances/admins/sign-up-screen-visited/",
|
"instances/admins/sign-up-screen-visited/",
|
||||||
|
@ -110,7 +110,9 @@ CSRF_COOKIE_SECURE = True
|
|||||||
CORS_ALLOW_CREDENTIALS = True
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
cors_origins_raw = os.environ.get("CORS_ALLOWED_ORIGINS", "")
|
cors_origins_raw = os.environ.get("CORS_ALLOWED_ORIGINS", "")
|
||||||
# filter out empty strings
|
# filter out empty strings
|
||||||
cors_allowed_origins = [origin.strip() for origin in cors_origins_raw.split(",") if origin.strip()]
|
cors_allowed_origins = [
|
||||||
|
origin.strip() for origin in cors_origins_raw.split(",") if origin.strip()
|
||||||
|
]
|
||||||
if cors_allowed_origins:
|
if cors_allowed_origins:
|
||||||
CORS_ALLOWED_ORIGINS = cors_allowed_origins
|
CORS_ALLOWED_ORIGINS = cors_allowed_origins
|
||||||
else:
|
else:
|
||||||
@ -286,6 +288,7 @@ CELERY_IMPORTS = (
|
|||||||
"plane.bgtasks.issue_automation_task",
|
"plane.bgtasks.issue_automation_task",
|
||||||
"plane.bgtasks.exporter_expired_task",
|
"plane.bgtasks.exporter_expired_task",
|
||||||
"plane.bgtasks.file_asset_task",
|
"plane.bgtasks.file_asset_task",
|
||||||
|
"plane.license.bgtasks.instance_verification_task",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sentry Settings
|
# Sentry Settings
|
||||||
@ -327,7 +330,11 @@ POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
|
|||||||
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
|
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
|
||||||
|
|
||||||
# License engine base url
|
# License engine base url
|
||||||
LICENSE_ENGINE_BASE_URL = os.environ.get("LICENSE_ENGINE_BASE_URL", "https://control-center.plane.so")
|
LICENSE_ENGINE_BASE_URL = os.environ.get(
|
||||||
|
"LICENSE_ENGINE_BASE_URL", "https://control-center.plane.so"
|
||||||
|
)
|
||||||
|
|
||||||
# instance key
|
# instance key
|
||||||
INSTANCE_KEY = os.environ.get("INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3")
|
INSTANCE_KEY = os.environ.get(
|
||||||
|
"INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3"
|
||||||
|
)
|
||||||
|
@ -38,7 +38,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
|||||||
setLoginCallBackURL(`${origin}/` as any);
|
setLoginCallBackURL(`${origin}/` as any);
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex justify-center items-center">
|
<div className="w-full">
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||||
>
|
>
|
||||||
|
@ -16,6 +16,7 @@ type Props = {
|
|||||||
email: string;
|
email: string;
|
||||||
handleStepChange: (step: ESignInSteps) => void;
|
handleStepChange: (step: ESignInSteps) => void;
|
||||||
handleSignInRedirection: () => Promise<void>;
|
handleSignInRedirection: () => Promise<void>;
|
||||||
|
isOnboarded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TCreatePasswordFormValues = {
|
type TCreatePasswordFormValues = {
|
||||||
@ -32,7 +33,7 @@ const defaultValues: TCreatePasswordFormValues = {
|
|||||||
const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
|
|
||||||
export const CreatePasswordForm: React.FC<Props> = (props) => {
|
export const CreatePasswordForm: React.FC<Props> = (props) => {
|
||||||
const { email, handleSignInRedirection } = props;
|
const { email, handleSignInRedirection, isOnboarded } = props;
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// form info
|
// form info
|
||||||
@ -76,9 +77,8 @@ export const CreatePasswordForm: React.FC<Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
||||||
Let{"'"}s get a new password
|
Get on your flight deck
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(handleCreatePassword)} className="mt-11 sm:w-96 mx-auto space-y-4">
|
<form onSubmit={handleSubmit(handleCreatePassword)} className="mt-11 sm:w-96 mx-auto space-y-4">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -97,37 +97,32 @@ export const CreatePasswordForm: React.FC<Props> = (props) => {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.email)}
|
hasError={Boolean(errors.email)}
|
||||||
placeholder="orville.wright@firstflight.com"
|
placeholder="orville.wright@firstflight.com"
|
||||||
className="w-full h-[46px] text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
className="w-full h-[46px] text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<Controller
|
||||||
<Controller
|
control={control}
|
||||||
control={control}
|
name="password"
|
||||||
name="password"
|
rules={{
|
||||||
rules={{
|
required: "Password is required",
|
||||||
required: "Password is required",
|
}}
|
||||||
}}
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
<Input
|
||||||
<Input
|
type="password"
|
||||||
type="password"
|
value={value}
|
||||||
value={value}
|
onChange={onChange}
|
||||||
onChange={onChange}
|
ref={ref}
|
||||||
ref={ref}
|
hasError={Boolean(errors.password)}
|
||||||
hasError={Boolean(errors.password)}
|
placeholder="Choose password"
|
||||||
placeholder="Create password"
|
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
minLength={8}
|
||||||
minLength={8}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
<p className="text-xs text-onboarding-text-200 mt-3 pb-2">
|
|
||||||
Whatever you choose now will be your account{"'"}s password until you change it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
|
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
|
||||||
{isSubmitting ? "Submitting..." : "Go to workspace"}
|
{isOnboarded ? "Go to workspace" : "Set up workspace"}
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-onboarding-text-200">
|
<p className="text-xs text-onboarding-text-200">
|
||||||
When you click the button above, you agree with our{" "}
|
When you click the button above, you agree with our{" "}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { XCircle } from "lucide-react";
|
import { XCircle } from "lucide-react";
|
||||||
// services
|
// services
|
||||||
@ -10,7 +10,7 @@ import { Button, Input } from "@plane/ui";
|
|||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IEmailCheckData, TEmailCheckTypes } from "types/auth";
|
import { IEmailCheckData } from "types/auth";
|
||||||
// constants
|
// constants
|
||||||
import { ESignInSteps } from "components/account";
|
import { ESignInSteps } from "components/account";
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ type Props = {
|
|||||||
updateEmail: (email: string) => void;
|
updateEmail: (email: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TEmailCodeFormValues = {
|
type TEmailFormValues = {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,17 +27,14 @@ const authService = new AuthService();
|
|||||||
|
|
||||||
export const EmailForm: React.FC<Props> = (props) => {
|
export const EmailForm: React.FC<Props> = (props) => {
|
||||||
const { handleStepChange, updateEmail } = props;
|
const { handleStepChange, updateEmail } = props;
|
||||||
// states
|
|
||||||
const [isCheckingEmail, setIsCheckingEmail] = useState<TEmailCheckTypes | null>(null);
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
formState: { errors, isValid },
|
formState: { errors, isSubmitting, isValid },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
} = useForm<TEmailFormValues>({
|
||||||
} = useForm<TEmailCodeFormValues>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
},
|
},
|
||||||
@ -45,31 +42,21 @@ export const EmailForm: React.FC<Props> = (props) => {
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleEmailCheck = async (type: TEmailCheckTypes) => {
|
const handleFormSubmit = async (data: TEmailFormValues) => {
|
||||||
setIsCheckingEmail(type);
|
|
||||||
|
|
||||||
const email = watch("email");
|
|
||||||
|
|
||||||
const payload: IEmailCheckData = {
|
const payload: IEmailCheckData = {
|
||||||
email,
|
email: data.email,
|
||||||
type,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// update the global email state
|
// update the global email state
|
||||||
updateEmail(email);
|
updateEmail(data.email);
|
||||||
|
|
||||||
await authService
|
await authService
|
||||||
.emailCheck(payload)
|
.emailCheck(payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
// if type is magic_code, send the user to magic sign in
|
// if the password has been autoset, send the user to magic sign-in
|
||||||
if (type === "magic_code") handleStepChange(ESignInSteps.UNIQUE_CODE);
|
if (res.is_password_autoset) handleStepChange(ESignInSteps.UNIQUE_CODE);
|
||||||
// if type is password, check if the user has a password set
|
// if the password has not been autoset, send them to password sign-in
|
||||||
if (type === "password") {
|
else handleStepChange(ESignInSteps.PASSWORD);
|
||||||
// if password is autoset, send them to set new password link
|
|
||||||
if (res.is_password_autoset) handleStepChange(ESignInSteps.SET_PASSWORD_LINK);
|
|
||||||
// if password is not autoset, send them to password form
|
|
||||||
else handleStepChange(ESignInSteps.PASSWORD);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -77,8 +64,7 @@ export const EmailForm: React.FC<Props> = (props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
.finally(() => setIsCheckingEmail(null));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -86,11 +72,11 @@ export const EmailForm: React.FC<Props> = (props) => {
|
|||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
||||||
Get on your flight deck
|
Get on your flight deck
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-center text-sm text-onboarding-text-200 mt-3">
|
<p className="text-center text-sm text-onboarding-text-200 mt-2.5">
|
||||||
Sign in with the email you used to sign up for Plane
|
Create or join a workspace. Start with your e-mail.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(() => {})} className="mt-5 sm:w-96 mx-auto">
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="mt-8 sm:w-96 mx-auto space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -122,30 +108,9 @@ export const EmailForm: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2.5 mt-4">
|
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
|
||||||
<Button
|
Continue
|
||||||
type="button"
|
</Button>
|
||||||
variant="primary"
|
|
||||||
className="w-full"
|
|
||||||
size="xl"
|
|
||||||
onClick={() => handleEmailCheck("magic_code")}
|
|
||||||
disabled={!isValid}
|
|
||||||
loading={Boolean(isCheckingEmail)}
|
|
||||||
>
|
|
||||||
{isCheckingEmail === "magic_code" ? "Sending code..." : "Send unique code"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline-primary"
|
|
||||||
className="w-full"
|
|
||||||
size="xl"
|
|
||||||
onClick={() => handleEmailCheck("password")}
|
|
||||||
disabled={!isValid}
|
|
||||||
loading={Boolean(isCheckingEmail)}
|
|
||||||
>
|
|
||||||
{isCheckingEmail === "password" ? "Loading..." : "Use password"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,8 @@ export * from "./create-password";
|
|||||||
export * from "./email-form";
|
export * from "./email-form";
|
||||||
export * from "./o-auth-options";
|
export * from "./o-auth-options";
|
||||||
export * from "./optional-set-password";
|
export * from "./optional-set-password";
|
||||||
export * from "./unique-code";
|
|
||||||
export * from "./password";
|
export * from "./password";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
export * from "./self-hosted-sign-in";
|
||||||
export * from "./set-password-link";
|
export * from "./set-password-link";
|
||||||
|
export * from "./unique-code";
|
||||||
|
@ -3,24 +3,20 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import { AuthService } from "services/auth.service";
|
import { AuthService } from "services/auth.service";
|
||||||
import { UserService } from "services/user.service";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { ESignInSteps, GithubLoginButton, GoogleLoginButton } from "components/account";
|
import { GithubLoginButton, GoogleLoginButton } from "components/account";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
updateEmail: (email: string) => void;
|
|
||||||
handleStepChange: (step: ESignInSteps) => void;
|
|
||||||
handleSignInRedirection: () => Promise<void>;
|
handleSignInRedirection: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
const userService = new UserService();
|
|
||||||
|
|
||||||
export const OAuthOptions: React.FC<Props> = observer((props) => {
|
export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||||
const { updateEmail, handleStepChange, handleSignInRedirection } = props;
|
const { handleSignInRedirection } = props;
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// mobx store
|
// mobx store
|
||||||
@ -38,14 +34,7 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
const response = await authService.socialAuth(socialAuthPayload);
|
const response = await authService.socialAuth(socialAuthPayload);
|
||||||
|
|
||||||
if (response) {
|
if (response) handleSignInRedirection();
|
||||||
const currentUser = await userService.currentUser();
|
|
||||||
|
|
||||||
updateEmail(currentUser.email);
|
|
||||||
|
|
||||||
if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD);
|
|
||||||
else handleSignInRedirection();
|
|
||||||
}
|
|
||||||
} else throw Error("Cant find credentials");
|
} else throw Error("Cant find credentials");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -66,14 +55,7 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
const response = await authService.socialAuth(socialAuthPayload);
|
const response = await authService.socialAuth(socialAuthPayload);
|
||||||
|
|
||||||
if (response) {
|
if (response) handleSignInRedirection();
|
||||||
const currentUser = await userService.currentUser();
|
|
||||||
|
|
||||||
updateEmail(currentUser.email);
|
|
||||||
|
|
||||||
if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD);
|
|
||||||
else handleSignInRedirection();
|
|
||||||
}
|
|
||||||
} else throw Error("Cant find credentials");
|
} else throw Error("Cant find credentials");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -87,11 +69,11 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex sm:w-96 items-center mt-4 mx-auto">
|
<div className="flex sm:w-96 items-center mt-4 mx-auto">
|
||||||
<hr className={`border-onboarding-border-100 w-full`} />
|
<hr className="border-onboarding-border-100 w-full" />
|
||||||
<p className="text-center text-sm text-onboarding-text-400 mx-3 flex-shrink-0">Or continue with</p>
|
<p className="text-center text-sm text-onboarding-text-400 mx-3 flex-shrink-0">Or continue with</p>
|
||||||
<hr className={`border-onboarding-border-100 w-full`} />
|
<hr className="border-onboarding-border-100 w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:flex-row sm:w-96 mx-auto overflow-hidden">
|
<div className="flex flex-col sm:flex-row items-center gap-2 pt-7 sm:w-96 mx-auto overflow-hidden">
|
||||||
{envConfig?.google_client_id && (
|
{envConfig?.google_client_id && (
|
||||||
<GoogleLoginButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
<GoogleLoginButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
||||||
)}
|
)}
|
||||||
|
@ -12,13 +12,14 @@ type Props = {
|
|||||||
email: string;
|
email: string;
|
||||||
handleStepChange: (step: ESignInSteps) => void;
|
handleStepChange: (step: ESignInSteps) => void;
|
||||||
handleSignInRedirection: () => Promise<void>;
|
handleSignInRedirection: () => Promise<void>;
|
||||||
|
isOnboarded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OptionalSetPasswordForm: React.FC<Props> = (props) => {
|
export const OptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||||
const { email, handleStepChange, handleSignInRedirection } = props;
|
const { email, handleStepChange, handleSignInRedirection, isOnboarded } = props;
|
||||||
// states
|
// states
|
||||||
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
||||||
|
// form info
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
formState: { errors, isValid },
|
formState: { errors, isValid },
|
||||||
@ -39,8 +40,8 @@ export const OptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">Set a password</h1>
|
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">Set a password</h1>
|
||||||
<p className="text-center text-sm text-onboarding-text-200 px-20 mt-3">
|
<p className="text-center text-sm text-onboarding-text-200 px-20 mt-2.5">
|
||||||
If you{"'"}d to do away with codes, set a password here
|
If you{"'"}d like to do away with codes, set a password here.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form className="mt-5 sm:w-96 mx-auto space-y-4">
|
<form className="mt-5 sm:w-96 mx-auto space-y-4">
|
||||||
@ -86,11 +87,13 @@ export const OptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
loading={isGoingToWorkspace}
|
loading={isGoingToWorkspace}
|
||||||
>
|
>
|
||||||
{isGoingToWorkspace ? "Going to app..." : "Go to workspace"}
|
{isOnboarded ? "Go to workspace" : "Set up workspace"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-onboarding-text-200">
|
<p className="text-xs text-onboarding-text-200">
|
||||||
When you click <span className="text-custom-primary-100">Go to workspace</span> above, you agree with our{" "}
|
When you click{" "}
|
||||||
|
<span className="text-custom-primary-100">{isOnboarded ? "Go to workspace" : "Set up workspace"}</span> above,
|
||||||
|
you agree with our{" "}
|
||||||
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
||||||
<span className="font-semibold underline">terms and conditions of service.</span>
|
<span className="font-semibold underline">terms and conditions of service.</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -11,7 +11,7 @@ import { Button, Input } from "@plane/ui";
|
|||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IEmailCheckData, IPasswordSignInData } from "types/auth";
|
import { IPasswordSignInData } from "types/auth";
|
||||||
// constants
|
// constants
|
||||||
import { ESignInSteps } from "components/account";
|
import { ESignInSteps } from "components/account";
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ const authService = new AuthService();
|
|||||||
export const PasswordForm: React.FC<Props> = (props) => {
|
export const PasswordForm: React.FC<Props> = (props) => {
|
||||||
const { email, updateEmail, handleStepChange, handleSignInRedirection } = props;
|
const { email, updateEmail, handleStepChange, handleSignInRedirection } = props;
|
||||||
// states
|
// states
|
||||||
|
const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false);
|
||||||
const [isSendingResetPasswordLink, setIsSendingResetPasswordLink] = useState(false);
|
const [isSendingResetPasswordLink, setIsSendingResetPasswordLink] = useState(false);
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -46,7 +47,6 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
formState: { dirtyFields, errors, isSubmitting, isValid },
|
formState: { dirtyFields, errors, isSubmitting, isValid },
|
||||||
getValues,
|
getValues,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
|
||||||
setError,
|
setError,
|
||||||
} = useForm<TPasswordFormValues>({
|
} = useForm<TPasswordFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -57,7 +57,9 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePasswordSignIn = async (formData: TPasswordFormValues) => {
|
const handleFormSubmit = async (formData: TPasswordFormValues) => {
|
||||||
|
updateEmail(formData.email);
|
||||||
|
|
||||||
const payload: IPasswordSignInData = {
|
const payload: IPasswordSignInData = {
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
@ -75,36 +77,6 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmailCheck = async (formData: TPasswordFormValues) => {
|
|
||||||
const payload: IEmailCheckData = {
|
|
||||||
email: formData.email,
|
|
||||||
type: "password",
|
|
||||||
};
|
|
||||||
|
|
||||||
await authService
|
|
||||||
.emailCheck(payload)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.is_password_autoset) handleStepChange(ESignInSteps.SET_PASSWORD_LINK);
|
|
||||||
else
|
|
||||||
reset({
|
|
||||||
email: formData.email,
|
|
||||||
password: "",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) =>
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: TPasswordFormValues) => {
|
|
||||||
if (dirtyFields.email) await handleEmailCheck(formData);
|
|
||||||
else await handlePasswordSignIn(formData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleForgotPassword = async () => {
|
const handleForgotPassword = async () => {
|
||||||
const emailFormValue = getValues("email");
|
const emailFormValue = getValues("email");
|
||||||
|
|
||||||
@ -130,6 +102,31 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
.finally(() => setIsSendingResetPasswordLink(false));
|
.finally(() => setIsSendingResetPasswordLink(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSendUniqueCode = async () => {
|
||||||
|
const emailFormValue = getValues("email");
|
||||||
|
|
||||||
|
const isEmailValid = checkEmailValidity(emailFormValue);
|
||||||
|
|
||||||
|
if (!isEmailValid) {
|
||||||
|
setError("email", { message: "Email is invalid" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSendingUniqueCode(true);
|
||||||
|
|
||||||
|
await authService
|
||||||
|
.generateUniqueCode({ email: emailFormValue })
|
||||||
|
.then(() => handleStepChange(ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD))
|
||||||
|
.catch((err) =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.finally(() => setIsSendingUniqueCode(false));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-onboarding-text-100">
|
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-onboarding-text-100">
|
||||||
@ -151,13 +148,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={onChange}
|
||||||
updateEmail(e.target.value);
|
|
||||||
onChange(e.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
if (dirtyFields.email) handleEmailCheck(getValues());
|
|
||||||
}}
|
|
||||||
hasError={Boolean(errors.email)}
|
hasError={Boolean(errors.email)}
|
||||||
placeholder="orville.wright@firstflight.com"
|
placeholder="orville.wright@firstflight.com"
|
||||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
||||||
@ -186,7 +177,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
hasError={Boolean(errors.password)}
|
hasError={Boolean(errors.password)}
|
||||||
placeholder="Enter password"
|
placeholder="Enter password"
|
||||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -199,15 +190,34 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
}`}
|
}`}
|
||||||
disabled={isSendingResetPasswordLink}
|
disabled={isSendingResetPasswordLink}
|
||||||
>
|
>
|
||||||
{isSendingResetPasswordLink ? "Sending link..." : "Forgot your password?"}
|
{isSendingResetPasswordLink ? "Sending link" : "Forgot your password?"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
|
<div className="grid sm:grid-cols-2 gap-2.5">
|
||||||
{isSubmitting ? "Signing in..." : "Go to workspace"}
|
<Button
|
||||||
</Button>
|
type="button"
|
||||||
|
onClick={handleSendUniqueCode}
|
||||||
|
variant="primary"
|
||||||
|
className="w-full"
|
||||||
|
size="xl"
|
||||||
|
loading={isSendingUniqueCode}
|
||||||
|
>
|
||||||
|
{isSendingUniqueCode ? "Sending code" : "Use unique code"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outline-primary"
|
||||||
|
className="w-full"
|
||||||
|
size="xl"
|
||||||
|
disabled={!isValid}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Go to workspace
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<p className="text-xs text-onboarding-text-200">
|
<p className="text-xs text-onboarding-text-200">
|
||||||
When you click the button above, you agree with our{" "}
|
When you click <span className="text-custom-primary-100">Go to workspace</span> above, you agree with our{" "}
|
||||||
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
||||||
<span className="font-semibold underline">terms and conditions of service.</span>
|
<span className="font-semibold underline">terms and conditions of service.</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
// components
|
// components
|
||||||
|
import { LatestFeatureBlock } from "components/common";
|
||||||
import {
|
import {
|
||||||
EmailForm,
|
EmailForm,
|
||||||
UniqueCodeForm,
|
UniqueCodeForm,
|
||||||
@ -10,6 +14,7 @@ import {
|
|||||||
OAuthOptions,
|
OAuthOptions,
|
||||||
OptionalSetPasswordForm,
|
OptionalSetPasswordForm,
|
||||||
CreatePasswordForm,
|
CreatePasswordForm,
|
||||||
|
SelfHostedSignInForm,
|
||||||
} from "components/account";
|
} from "components/account";
|
||||||
|
|
||||||
export enum ESignInSteps {
|
export enum ESignInSteps {
|
||||||
@ -19,64 +24,96 @@ export enum ESignInSteps {
|
|||||||
UNIQUE_CODE = "UNIQUE_CODE",
|
UNIQUE_CODE = "UNIQUE_CODE",
|
||||||
OPTIONAL_SET_PASSWORD = "OPTIONAL_SET_PASSWORD",
|
OPTIONAL_SET_PASSWORD = "OPTIONAL_SET_PASSWORD",
|
||||||
CREATE_PASSWORD = "CREATE_PASSWORD",
|
CREATE_PASSWORD = "CREATE_PASSWORD",
|
||||||
|
USE_UNIQUE_CODE_FROM_PASSWORD = "USE_UNIQUE_CODE_FROM_PASSWORD",
|
||||||
}
|
}
|
||||||
|
|
||||||
const OAUTH_HIDDEN_STEPS = [ESignInSteps.OPTIONAL_SET_PASSWORD, ESignInSteps.CREATE_PASSWORD];
|
const OAUTH_HIDDEN_STEPS = [ESignInSteps.OPTIONAL_SET_PASSWORD, ESignInSteps.CREATE_PASSWORD];
|
||||||
|
|
||||||
export const SignInRoot = () => {
|
export const SignInRoot = observer(() => {
|
||||||
// states
|
// states
|
||||||
const [signInStep, setSignInStep] = useState<ESignInSteps>(ESignInSteps.EMAIL);
|
const [signInStep, setSignInStep] = useState<ESignInSteps>(ESignInSteps.EMAIL);
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
|
const [isOnboarded, setIsOnboarded] = useState(false);
|
||||||
// sign in redirection hook
|
// sign in redirection hook
|
||||||
const { handleRedirection } = useSignInRedirection();
|
const { handleRedirection } = useSignInRedirection();
|
||||||
|
// mobx store
|
||||||
|
const {
|
||||||
|
appConfig: { envConfig },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mx-auto flex flex-col">
|
<div className="mx-auto flex flex-col">
|
||||||
{signInStep === ESignInSteps.EMAIL && (
|
{envConfig?.is_self_managed ? (
|
||||||
<EmailForm handleStepChange={(step) => setSignInStep(step)} updateEmail={(newEmail) => setEmail(newEmail)} />
|
<SelfHostedSignInForm
|
||||||
)}
|
|
||||||
{signInStep === ESignInSteps.PASSWORD && (
|
|
||||||
<PasswordForm
|
|
||||||
email={email}
|
email={email}
|
||||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||||
handleStepChange={(step) => setSignInStep(step)}
|
|
||||||
handleSignInRedirection={handleRedirection}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{signInStep === ESignInSteps.SET_PASSWORD_LINK && (
|
|
||||||
<SetPasswordLink email={email} updateEmail={(newEmail) => setEmail(newEmail)} />
|
|
||||||
)}
|
|
||||||
{signInStep === ESignInSteps.UNIQUE_CODE && (
|
|
||||||
<UniqueCodeForm
|
|
||||||
email={email}
|
|
||||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
|
||||||
handleStepChange={(step) => setSignInStep(step)}
|
|
||||||
handleSignInRedirection={handleRedirection}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && (
|
|
||||||
<OptionalSetPasswordForm
|
|
||||||
email={email}
|
|
||||||
handleStepChange={(step) => setSignInStep(step)}
|
|
||||||
handleSignInRedirection={handleRedirection}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{signInStep === ESignInSteps.CREATE_PASSWORD && (
|
|
||||||
<CreatePasswordForm
|
|
||||||
email={email}
|
|
||||||
handleStepChange={(step) => setSignInStep(step)}
|
|
||||||
handleSignInRedirection={handleRedirection}
|
handleSignInRedirection={handleRedirection}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{signInStep === ESignInSteps.EMAIL && (
|
||||||
|
<EmailForm
|
||||||
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
|
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{signInStep === ESignInSteps.PASSWORD && (
|
||||||
|
<PasswordForm
|
||||||
|
email={email}
|
||||||
|
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||||
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
|
handleSignInRedirection={handleRedirection}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{signInStep === ESignInSteps.SET_PASSWORD_LINK && (
|
||||||
|
<SetPasswordLink email={email} updateEmail={(newEmail) => setEmail(newEmail)} />
|
||||||
|
)}
|
||||||
|
{signInStep === ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD && (
|
||||||
|
<UniqueCodeForm
|
||||||
|
email={email}
|
||||||
|
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||||
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
|
handleSignInRedirection={handleRedirection}
|
||||||
|
submitButtonLabel="Go to workspace"
|
||||||
|
showTermsAndConditions
|
||||||
|
updateUserOnboardingStatus={(value) => setIsOnboarded(value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{signInStep === ESignInSteps.UNIQUE_CODE && (
|
||||||
|
<UniqueCodeForm
|
||||||
|
email={email}
|
||||||
|
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||||
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
|
handleSignInRedirection={handleRedirection}
|
||||||
|
updateUserOnboardingStatus={(value) => setIsOnboarded(value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && (
|
||||||
|
<OptionalSetPasswordForm
|
||||||
|
email={email}
|
||||||
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
|
handleSignInRedirection={handleRedirection}
|
||||||
|
isOnboarded={isOnboarded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{signInStep === ESignInSteps.CREATE_PASSWORD && (
|
||||||
|
<CreatePasswordForm
|
||||||
|
email={email}
|
||||||
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
|
handleSignInRedirection={handleRedirection}
|
||||||
|
isOnboarded={isOnboarded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!OAUTH_HIDDEN_STEPS.includes(signInStep) && (
|
{isOAuthEnabled && !OAUTH_HIDDEN_STEPS.includes(signInStep) && (
|
||||||
<OAuthOptions
|
<OAuthOptions handleSignInRedirection={handleRedirection} />
|
||||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
|
||||||
handleStepChange={(step) => setSignInStep(step)}
|
|
||||||
handleSignInRedirection={handleRedirection}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
<LatestFeatureBlock />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
139
web/components/account/sign-in-forms/self-hosted-sign-in.tsx
Normal file
139
web/components/account/sign-in-forms/self-hosted-sign-in.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { XCircle } from "lucide-react";
|
||||||
|
// services
|
||||||
|
import { AuthService } from "services/auth.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// ui
|
||||||
|
import { Button, Input } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
|
// types
|
||||||
|
import { IPasswordSignInData } from "types/auth";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
email: string;
|
||||||
|
updateEmail: (email: string) => void;
|
||||||
|
handleSignInRedirection: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TPasswordFormValues = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues: TPasswordFormValues = {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
|
export const SelfHostedSignInForm: React.FC<Props> = (props) => {
|
||||||
|
const { email, updateEmail, handleSignInRedirection } = props;
|
||||||
|
// toast alert
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
// form info
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { dirtyFields, errors, isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm<TPasswordFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
...defaultValues,
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormSubmit = async (formData: TPasswordFormValues) => {
|
||||||
|
const payload: IPasswordSignInData = {
|
||||||
|
email: formData.email,
|
||||||
|
password: formData.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateEmail(formData.email);
|
||||||
|
|
||||||
|
await authService
|
||||||
|
.passwordSignIn(payload)
|
||||||
|
.then(async () => await handleSignInRedirection())
|
||||||
|
.catch((err) =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-onboarding-text-100">
|
||||||
|
Get on your flight deck
|
||||||
|
</h1>
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="mt-11 sm:w-96 mx-auto space-y-4">
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="email"
|
||||||
|
rules={{
|
||||||
|
required: "Email is required",
|
||||||
|
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||||
|
}}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<div className="flex items-center relative rounded-md bg-onboarding-background-200">
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
hasError={Boolean(errors.email)}
|
||||||
|
placeholder="orville.wright@firstflight.com"
|
||||||
|
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
||||||
|
/>
|
||||||
|
{value.length > 0 && (
|
||||||
|
<XCircle
|
||||||
|
className="h-5 w-5 absolute stroke-custom-text-400 hover:cursor-pointer right-3"
|
||||||
|
onClick={() => onChange("")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="password"
|
||||||
|
rules={{
|
||||||
|
required: dirtyFields.email ? false : "Password is required",
|
||||||
|
}}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
hasError={Boolean(errors.password)}
|
||||||
|
placeholder="Enter password"
|
||||||
|
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" variant="primary" className="w-full" size="xl" loading={isSubmitting}>
|
||||||
|
Go to workspace
|
||||||
|
</Button>
|
||||||
|
<p className="text-xs text-onboarding-text-200">
|
||||||
|
When you click the button above, you agree with our{" "}
|
||||||
|
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
||||||
|
<span className="font-semibold underline">terms and conditions of service.</span>
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { XCircle } from "lucide-react";
|
|
||||||
// services
|
// services
|
||||||
import { AuthService } from "services/auth.service";
|
import { AuthService } from "services/auth.service";
|
||||||
// hooks
|
// hooks
|
||||||
@ -22,14 +20,12 @@ const authService = new AuthService();
|
|||||||
|
|
||||||
export const SetPasswordLink: React.FC<Props> = (props) => {
|
export const SetPasswordLink: React.FC<Props> = (props) => {
|
||||||
const { email, updateEmail } = props;
|
const { email, updateEmail } = props;
|
||||||
// states
|
|
||||||
const [isSendingNewLink, setIsSendingNewLink] = useState(false);
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
formState: { errors, isValid },
|
formState: { errors, isSubmitting, isValid },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -40,17 +36,14 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSendNewLink = async (formData: { email: string }) => {
|
const handleSendNewLink = async (formData: { email: string }) => {
|
||||||
setIsSendingNewLink(true);
|
|
||||||
|
|
||||||
updateEmail(formData.email);
|
updateEmail(formData.email);
|
||||||
|
|
||||||
const payload: IEmailCheckData = {
|
const payload: IEmailCheckData = {
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
type: "password",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await authService
|
await authService
|
||||||
.emailCheck(payload)
|
.sendResetPasswordLink(payload)
|
||||||
.then(() =>
|
.then(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -64,8 +57,7 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
.finally(() => setIsSendingNewLink(false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -73,7 +65,7 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
|||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
||||||
Get on your flight deck
|
Get on your flight deck
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-center text-sm text-onboarding-text-200 px-20 mt-3">
|
<p className="text-center text-sm text-onboarding-text-200 px-20 mt-2.5">
|
||||||
We have sent a link to <span className="font-semibold text-custom-primary-100">{email},</span> so you can set a
|
We have sent a link to <span className="font-semibold text-custom-primary-100">{email},</span> so you can set a
|
||||||
password
|
password
|
||||||
</p>
|
</p>
|
||||||
@ -87,45 +79,24 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
|||||||
required: "Email is required",
|
required: "Email is required",
|
||||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||||
}}
|
}}
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="flex items-center relative rounded-md bg-onboarding-background-200">
|
<Input
|
||||||
<Input
|
id="email"
|
||||||
id="email"
|
name="email"
|
||||||
name="email"
|
type="email"
|
||||||
type="email"
|
value={value}
|
||||||
value={value}
|
onChange={onChange}
|
||||||
onChange={onChange}
|
hasError={Boolean(errors.email)}
|
||||||
ref={ref}
|
placeholder="orville.wright@firstflight.com"
|
||||||
hasError={Boolean(errors.email)}
|
className="w-full h-[46px] text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||||
placeholder="orville.wright@firstflight.com"
|
disabled
|
||||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
/>
|
||||||
/>
|
|
||||||
{value.length > 0 && (
|
|
||||||
<XCircle
|
|
||||||
className="h-5 w-5 absolute stroke-custom-text-400 hover:cursor-pointer right-3"
|
|
||||||
onClick={() => onChange("")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
|
||||||
type="submit"
|
{isSubmitting ? "Sending new link" : "Get link again"}
|
||||||
variant="primary"
|
|
||||||
className="w-full"
|
|
||||||
size="xl"
|
|
||||||
disabled={!isValid}
|
|
||||||
loading={isSendingNewLink}
|
|
||||||
>
|
|
||||||
{isSendingNewLink ? "Sending new link..." : "Get link again"}
|
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-onboarding-text-200">
|
|
||||||
When you click the button above, you agree with our{" "}
|
|
||||||
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
|
||||||
<span className="font-semibold underline">terms and conditions of service.</span>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -22,6 +22,9 @@ type Props = {
|
|||||||
updateEmail: (email: string) => void;
|
updateEmail: (email: string) => void;
|
||||||
handleStepChange: (step: ESignInSteps) => void;
|
handleStepChange: (step: ESignInSteps) => void;
|
||||||
handleSignInRedirection: () => Promise<void>;
|
handleSignInRedirection: () => Promise<void>;
|
||||||
|
submitButtonLabel?: string;
|
||||||
|
showTermsAndConditions?: boolean;
|
||||||
|
updateUserOnboardingStatus: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TUniqueCodeFormValues = {
|
type TUniqueCodeFormValues = {
|
||||||
@ -39,7 +42,15 @@ const authService = new AuthService();
|
|||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
export const UniqueCodeForm: React.FC<Props> = (props) => {
|
export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||||
const { email, updateEmail, handleStepChange, handleSignInRedirection } = props;
|
const {
|
||||||
|
email,
|
||||||
|
updateEmail,
|
||||||
|
handleStepChange,
|
||||||
|
handleSignInRedirection,
|
||||||
|
submitButtonLabel = "Continue",
|
||||||
|
showTermsAndConditions = false,
|
||||||
|
updateUserOnboardingStatus,
|
||||||
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||||
// toast alert
|
// toast alert
|
||||||
@ -74,6 +85,8 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
const currentUser = await userService.currentUser();
|
const currentUser = await userService.currentUser();
|
||||||
|
|
||||||
|
updateUserOnboardingStatus(currentUser.is_onboarded);
|
||||||
|
|
||||||
if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD);
|
if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD);
|
||||||
else await handleSignInRedirection();
|
else await handleSignInRedirection();
|
||||||
})
|
})
|
||||||
@ -86,15 +99,15 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendNewLink = async (formData: TUniqueCodeFormValues) => {
|
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
|
||||||
const payload: IEmailCheckData = {
|
const payload: IEmailCheckData = {
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
type: "magic_code",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await authService
|
await authService
|
||||||
.emailCheck(payload)
|
.generateUniqueCode(payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
setResendCodeTimer(30);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -116,25 +129,30 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: TUniqueCodeFormValues) => {
|
const handleFormSubmit = async (formData: TUniqueCodeFormValues) => {
|
||||||
if (dirtyFields.email) await handleSendNewLink(formData);
|
updateEmail(formData.email);
|
||||||
|
|
||||||
|
if (dirtyFields.email) await handleSendNewCode(formData);
|
||||||
else await handleUniqueCodeSignIn(formData);
|
else await handleUniqueCodeSignIn(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRequestNewCode = async () => {
|
const handleRequestNewCode = async () => {
|
||||||
setIsRequestingNewCode(true);
|
setIsRequestingNewCode(true);
|
||||||
|
|
||||||
await handleSendNewLink(getValues())
|
await handleSendNewCode(getValues())
|
||||||
.then(() => setResendCodeTimer(30))
|
.then(() => setResendCodeTimer(30))
|
||||||
.finally(() => setIsRequestingNewCode(false));
|
.finally(() => setIsRequestingNewCode(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
|
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
|
||||||
|
const hasEmailChanged = dirtyFields.email;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">Moving to the runway</h1>
|
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
||||||
<p className="text-center text-sm text-onboarding-text-200 mt-3">
|
Get on your flight deck
|
||||||
Paste the code you got at <span className="font-semibold text-custom-primary-100">{email}</span> below
|
</h1>
|
||||||
|
<p className="text-center text-sm text-onboarding-text-200 mt-2.5">
|
||||||
|
Paste the code you got at <span className="font-semibold text-custom-primary-100">{email}</span> below.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="mt-5 sm:w-96 mx-auto space-y-4">
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="mt-5 sm:w-96 mx-auto space-y-4">
|
||||||
@ -153,12 +171,9 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={onChange}
|
||||||
updateEmail(e.target.value);
|
|
||||||
onChange(e.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (dirtyFields.email) handleSendNewLink(getValues());
|
if (hasEmailChanged) handleSendNewCode(getValues());
|
||||||
}}
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.email)}
|
hasError={Boolean(errors.email)}
|
||||||
@ -174,7 +189,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{dirtyFields.email && (
|
{hasEmailChanged && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-xs text-onboarding-text-300 mt-1.5 flex items-center gap-1 outline-none bg-transparent border-none"
|
className="text-xs text-onboarding-text-300 mt-1.5 flex items-center gap-1 outline-none bg-transparent border-none"
|
||||||
@ -188,7 +203,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="token"
|
name="token"
|
||||||
rules={{
|
rules={{
|
||||||
required: dirtyFields.email ? false : "Code is required",
|
required: hasEmailChanged ? false : "Code is required",
|
||||||
}}
|
}}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<Input
|
<Input
|
||||||
@ -196,7 +211,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
hasError={Boolean(errors.token)}
|
hasError={Boolean(errors.token)}
|
||||||
placeholder="gets-sets-flys"
|
placeholder="gets-sets-flys"
|
||||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -214,7 +229,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
{resendTimerCode > 0
|
{resendTimerCode > 0
|
||||||
? `Request new code in ${resendTimerCode}s`
|
? `Request new code in ${resendTimerCode}s`
|
||||||
: isRequestingNewCode
|
: isRequestingNewCode
|
||||||
? "Requesting new code..."
|
? "Requesting new code"
|
||||||
: "Request new code"}
|
: "Request new code"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -224,17 +239,19 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
size="xl"
|
size="xl"
|
||||||
disabled={!isValid || dirtyFields.email}
|
disabled={!isValid || hasEmailChanged}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Signing in..." : "Confirm"}
|
{submitButtonLabel}
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-onboarding-text-200">
|
{showTermsAndConditions && (
|
||||||
When you click Confirm above, you agree with our{" "}
|
<p className="text-xs text-onboarding-text-200">
|
||||||
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
When you click the button above, you agree with our{" "}
|
||||||
<span className="font-semibold underline">terms and conditions of service.</span>
|
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
||||||
</Link>
|
<span className="font-semibold underline">terms and conditions of service.</span>
|
||||||
</p>
|
</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -25,7 +25,7 @@ export const LatestFeatureBlock = () => {
|
|||||||
<Image
|
<Image
|
||||||
src={latestFeatures}
|
src={latestFeatures}
|
||||||
alt="Plane Issues"
|
alt="Plane Issues"
|
||||||
className={`rounded-md h-full ml-8 -mt-2 ${
|
className={`rounded-md h-full ml-10 -mt-2 ${
|
||||||
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||||
} `}
|
} `}
|
||||||
/>
|
/>
|
||||||
|
@ -17,10 +17,10 @@ export const InstanceNotReady = () => {
|
|||||||
<div className="h-full bg-onboarding-gradient-100 md:w-2/3 sm:w-4/5 px-4 pt-4 rounded-t-md mx-auto shadow-sm border-x border-t border-custom-border-100">
|
<div className="h-full bg-onboarding-gradient-100 md:w-2/3 sm:w-4/5 px-4 pt-4 rounded-t-md mx-auto shadow-sm border-x border-t border-custom-border-100">
|
||||||
<div className="relative px-7 sm:px-0 bg-onboarding-gradient-200 h-full rounded-t-md">
|
<div className="relative px-7 sm:px-0 bg-onboarding-gradient-200 h-full rounded-t-md">
|
||||||
<div className="flex items-center py-10 justify-center">
|
<div className="flex items-center py-10 justify-center">
|
||||||
<Image src={planeLogo} className="h-44 w-full" alt="Plane logo" />
|
<Image src={planeLogo} className="h-[44px] w-full" alt="Plane logo" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-20">
|
<div className="mt-20">
|
||||||
<Image src={instanceNotReady} className="h-56 w-full" alt="Instance not ready" />
|
<Image src={instanceNotReady} className="w-full" alt="Instance not ready" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-5 items-center py-12 pb-20 w-full">
|
<div className="flex flex-col gap-5 items-center py-12 pb-20 w-full">
|
||||||
<h3 className="text-2xl font-medium">Your Plane instance isn{"'"}t ready yet</h3>
|
<h3 className="text-2xl font-medium">Your Plane instance isn{"'"}t ready yet</h3>
|
||||||
|
@ -55,7 +55,7 @@ export const InstanceSetupDone = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button size="lg" prependIcon={<UserCog2 />} onClick={redirectToGodMode} loading={isRedirecting}>
|
<Button size="lg" prependIcon={<UserCog2 />} onClick={redirectToGodMode} loading={isRedirecting}>
|
||||||
{isRedirecting ? "Redirecting..." : "Go to God Mode"}
|
Go to God Mode
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,160 +0,0 @@
|
|||||||
import { FC, useState } from "react";
|
|
||||||
import { useForm, Controller } from "react-hook-form";
|
|
||||||
// ui
|
|
||||||
import { Input, Button } from "@plane/ui";
|
|
||||||
// icons
|
|
||||||
import { XCircle } from "lucide-react";
|
|
||||||
// services
|
|
||||||
import { AuthService } from "services/auth.service";
|
|
||||||
const authService = new AuthService();
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
import useTimer from "hooks/use-timer";
|
|
||||||
|
|
||||||
export interface InstanceSetupEmailCodeFormValues {
|
|
||||||
email: string;
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInstanceSetupEmailCodeForm {
|
|
||||||
email: string;
|
|
||||||
handleNextStep: () => void;
|
|
||||||
moveBack: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InstanceSetupEmailCodeForm: FC<IInstanceSetupEmailCodeForm> = (props) => {
|
|
||||||
const { handleNextStep, email, moveBack } = props;
|
|
||||||
// states
|
|
||||||
const [isResendingCode, setIsResendingCode] = useState(false);
|
|
||||||
// form info
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
} = useForm<InstanceSetupEmailCodeFormValues>({
|
|
||||||
defaultValues: {
|
|
||||||
email,
|
|
||||||
token: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// hooks
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
const { timer, setTimer } = useTimer(30);
|
|
||||||
// computed
|
|
||||||
const isResendDisabled = timer > 0 || isResendingCode;
|
|
||||||
|
|
||||||
const handleEmailCodeFormSubmit = async (formValues: InstanceSetupEmailCodeFormValues) =>
|
|
||||||
await authService
|
|
||||||
.instanceMagicSignIn({ key: `magic_${formValues.email}`, token: formValues.token })
|
|
||||||
.then(() => {
|
|
||||||
reset();
|
|
||||||
handleNextStep();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const resendMagicCode = async () => {
|
|
||||||
setIsResendingCode(true);
|
|
||||||
|
|
||||||
await authService
|
|
||||||
.instanceAdminEmailCode({ email })
|
|
||||||
.then(() => setTimer(30))
|
|
||||||
.catch((err) => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => setIsResendingCode(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(handleEmailCodeFormSubmit)}>
|
|
||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
|
||||||
Let{"'"}s secure your instance
|
|
||||||
</h1>
|
|
||||||
<p className="text-center text-sm text-onboarding-text-200 mt-3">
|
|
||||||
Paste the code you got at
|
|
||||||
<br />
|
|
||||||
<span className="text-custom-primary-100 font-semibold">{email}</span> below.
|
|
||||||
</p>
|
|
||||||
<div className="relative mt-5 w-full sm:w-96 mx-auto space-y-4">
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
name="email"
|
|
||||||
control={control}
|
|
||||||
rules={{
|
|
||||||
required: "Email address is required",
|
|
||||||
validate: (value) =>
|
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
|
||||||
value
|
|
||||||
) || "Email address is not valid",
|
|
||||||
}}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<div className="flex items-center relative rounded-md bg-onboarding-background-200">
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
disabled
|
|
||||||
placeholder="orville.wright@firstflight.com"
|
|
||||||
className={`w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12`}
|
|
||||||
/>
|
|
||||||
<XCircle
|
|
||||||
className="h-5 w-5 absolute stroke-custom-text-400 hover:cursor-pointer right-3"
|
|
||||||
onClick={() => moveBack()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="w-full text-right">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={resendMagicCode}
|
|
||||||
className={`text-xs ${
|
|
||||||
isResendDisabled ? "text-onboarding-text-300" : "text-onboarding-text-200 hover:text-custom-primary-100"
|
|
||||||
}`}
|
|
||||||
disabled={isResendDisabled}
|
|
||||||
>
|
|
||||||
{timer > 0
|
|
||||||
? `Request new code in ${timer}s`
|
|
||||||
: isSubmitting
|
|
||||||
? "Requesting new code..."
|
|
||||||
: "Request new code"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name="token"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: true }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<div className={`flex items-center relative rounded-md bg-onboarding-background-200 mb-4`}>
|
|
||||||
<Input
|
|
||||||
id="token"
|
|
||||||
name="token"
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
placeholder="gets-sets-fays"
|
|
||||||
className="border-onboarding-border-100 h-[46px] w-full "
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button variant="primary" className="w-full" size="xl" type="submit" loading={isSubmitting}>
|
|
||||||
{isSubmitting ? "Verifying..." : "Next step"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,3 +1,2 @@
|
|||||||
export * from "./email-code-form";
|
|
||||||
export * from "./email-form";
|
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
export * from "./sign-in-form";
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useForm, Controller } from "react-hook-form";
|
|
||||||
// ui
|
|
||||||
import { Input, Button } from "@plane/ui";
|
|
||||||
// icons
|
|
||||||
import { XCircle } from "lucide-react";
|
|
||||||
// services
|
|
||||||
import { AuthService } from "services/auth.service";
|
|
||||||
const authService = new AuthService();
|
|
||||||
|
|
||||||
export interface InstanceSetupPasswordFormValues {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInstanceSetupPasswordForm {
|
|
||||||
email: string;
|
|
||||||
onNextStep: () => void;
|
|
||||||
resetSteps: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InstanceSetupPasswordForm: React.FC<IInstanceSetupPasswordForm> = (props) => {
|
|
||||||
const { onNextStep, email, resetSteps } = props;
|
|
||||||
// form info
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
} = useForm<InstanceSetupPasswordFormValues>({
|
|
||||||
defaultValues: {
|
|
||||||
email,
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handlePasswordSubmit = (formData: InstanceSetupPasswordFormValues) =>
|
|
||||||
authService.setInstanceAdminPassword({ password: formData.password }).then(() => {
|
|
||||||
onNextStep();
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(handlePasswordSubmit)}>
|
|
||||||
<div className="pb-2">
|
|
||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
|
||||||
Moving to the runway
|
|
||||||
</h1>
|
|
||||||
<p className="text-center text-sm text-onboarding-text-200 mt-3">
|
|
||||||
Let{"'"}s set a password so you can do away with codes.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="relative mt-5 w-full sm:w-96 mx-auto space-y-4">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="email"
|
|
||||||
rules={{
|
|
||||||
required: "Email is required",
|
|
||||||
validate: (value) =>
|
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
|
||||||
value
|
|
||||||
) || "Email address is not valid",
|
|
||||||
}}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<div className={`flex items-center relative rounded-md bg-onboarding-background-200`}>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
placeholder="orville.wright@firstflight.com"
|
|
||||||
className={`w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12`}
|
|
||||||
/>
|
|
||||||
<XCircle
|
|
||||||
className="h-5 w-5 absolute stroke-custom-text-400 hover:cursor-pointer right-3"
|
|
||||||
onClick={() => resetSteps()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="password"
|
|
||||||
rules={{
|
|
||||||
required: "Password is required",
|
|
||||||
minLength: {
|
|
||||||
value: 8,
|
|
||||||
message: "Minimum 8 characters required",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<div className={`flex items-center relative rounded-md bg-onboarding-background-200`}>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
hasError={Boolean(errors.password)}
|
|
||||||
placeholder="Enter your password..."
|
|
||||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<p className="text-xs mt-3 text-onboarding-text-200 pb-2">
|
|
||||||
Whatever you choose now will be your account{"'"}s password
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button variant="primary" className="w-full mt-4" size="xl" type="submit" loading={isSubmitting}>
|
|
||||||
{isSubmitting ? "Submitting..." : "Next step"}
|
|
||||||
</Button>
|
|
||||||
<p className="text-xs text-onboarding-text-200">
|
|
||||||
When you click the button above, you agree with our{" "}
|
|
||||||
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
|
||||||
<span className="font-semibold underline">terms and conditions of service.</span>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,63 +1,25 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
// components
|
// components
|
||||||
import { InstanceSetupEmailCodeForm } from "./email-code-form";
|
|
||||||
import { InstanceSetupEmailForm } from "./email-form";
|
|
||||||
import { InstanceSetupPasswordForm } from "./password-form";
|
|
||||||
import { LatestFeatureBlock } from "components/common";
|
import { LatestFeatureBlock } from "components/common";
|
||||||
import { InstanceSetupDone } from "components/instance";
|
import { InstanceSetupDone, InstanceSetupSignInForm } from "components/instance";
|
||||||
|
|
||||||
export enum EInstanceSetupSteps {
|
export enum EInstanceSetupSteps {
|
||||||
EMAIL = "EMAIL",
|
SIGN_IN = "SIGN_IN",
|
||||||
VERIFY_CODE = "VERIFY_CODE",
|
|
||||||
PASSWORD = "PASSWORD",
|
|
||||||
DONE = "DONE",
|
DONE = "DONE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstanceSetupFormRoot = () => {
|
export const InstanceSetupFormRoot = () => {
|
||||||
// states
|
// states
|
||||||
const [setupStep, setSetupStep] = useState(EInstanceSetupSteps.EMAIL);
|
const [setupStep, setSetupStep] = useState(EInstanceSetupSteps.SIGN_IN);
|
||||||
const [email, setEmail] = useState<string>("");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{setupStep === EInstanceSetupSteps.DONE ? (
|
{setupStep === EInstanceSetupSteps.DONE && <InstanceSetupDone />}
|
||||||
<InstanceSetupDone />
|
{setupStep === EInstanceSetupSteps.SIGN_IN && (
|
||||||
) : (
|
|
||||||
<div className="h-full bg-onboarding-gradient-100 md:w-2/3 sm:w-4/5 px-4 pt-4 rounded-t-md mx-auto shadow-sm border-x border-t border-custom-border-200">
|
<div className="h-full bg-onboarding-gradient-100 md:w-2/3 sm:w-4/5 px-4 pt-4 rounded-t-md mx-auto shadow-sm border-x border-t border-custom-border-200">
|
||||||
<div className="bg-onboarding-gradient-200 h-full pt-24 pb-56 rounded-t-md overflow-auto">
|
<div className="bg-onboarding-gradient-200 h-full pt-24 pb-56 rounded-t-md overflow-auto">
|
||||||
<div className="mx-auto flex flex-col">
|
<div className="mx-auto flex flex-col">
|
||||||
{setupStep === EInstanceSetupSteps.EMAIL && (
|
<InstanceSetupSignInForm handleNextStep={() => setSetupStep(EInstanceSetupSteps.DONE)} />
|
||||||
<InstanceSetupEmailForm
|
|
||||||
handleNextStep={(email) => {
|
|
||||||
setEmail(email);
|
|
||||||
setSetupStep(EInstanceSetupSteps.VERIFY_CODE);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{setupStep === EInstanceSetupSteps.VERIFY_CODE && (
|
|
||||||
<InstanceSetupEmailCodeForm
|
|
||||||
email={email}
|
|
||||||
handleNextStep={() => {
|
|
||||||
setSetupStep(EInstanceSetupSteps.PASSWORD);
|
|
||||||
}}
|
|
||||||
moveBack={() => {
|
|
||||||
setSetupStep(EInstanceSetupSteps.EMAIL);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{setupStep === EInstanceSetupSteps.PASSWORD && (
|
|
||||||
<InstanceSetupPasswordForm
|
|
||||||
email={email}
|
|
||||||
onNextStep={() => {
|
|
||||||
setSetupStep(EInstanceSetupSteps.DONE);
|
|
||||||
}}
|
|
||||||
resetSteps={() => {
|
|
||||||
setSetupStep(EInstanceSetupSteps.EMAIL);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<LatestFeatureBlock />
|
<LatestFeatureBlock />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// ui
|
// ui
|
||||||
import { Input, Button } from "@plane/ui";
|
import { Input, Button } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -9,37 +11,48 @@ import { AuthService } from "services/auth.service";
|
|||||||
const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
// helpers
|
||||||
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
|
|
||||||
export interface InstanceSetupEmailFormValues {
|
interface InstanceSetupEmailFormValues {
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInstanceSetupEmailForm {
|
export interface IInstanceSetupEmailForm {
|
||||||
handleNextStep: (email: string) => void;
|
handleNextStep: (email: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
export const InstanceSetupSignInForm: FC<IInstanceSetupEmailForm> = (props) => {
|
||||||
const { handleNextStep } = props;
|
const { handleNextStep } = props;
|
||||||
|
const {
|
||||||
|
user: { fetchCurrentUser },
|
||||||
|
} = useMobxStore();
|
||||||
// form info
|
// form info
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
reset,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
} = useForm<InstanceSetupEmailFormValues>({
|
} = useForm<InstanceSetupEmailFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
|
password: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const handleEmailFormSubmit = (formValues: InstanceSetupEmailFormValues) =>
|
const handleFormSubmit = async (formValues: InstanceSetupEmailFormValues) => {
|
||||||
authService
|
const payload = {
|
||||||
.instanceAdminEmailCode({ email: formValues.email })
|
email: formValues.email,
|
||||||
.then(() => {
|
password: formValues.password,
|
||||||
reset();
|
};
|
||||||
|
|
||||||
|
await authService
|
||||||
|
.instanceAdminSignIn(payload)
|
||||||
|
.then(async () => {
|
||||||
|
await fetchCurrentUser();
|
||||||
handleNextStep(formValues.email);
|
handleNextStep(formValues.email);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -49,9 +62,10 @@ export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
|||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleEmailFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
||||||
Let{"'"}s secure your instance
|
Let{"'"}s secure your instance
|
||||||
</h1>
|
</h1>
|
||||||
@ -66,13 +80,10 @@ export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
required: "Email address is required",
|
required: "Email address is required",
|
||||||
validate: (value) =>
|
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
|
||||||
value
|
|
||||||
) || "Email address is not valid",
|
|
||||||
}}
|
}}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className={`flex items-center relative rounded-md bg-onboarding-background-200`}>
|
<div className="flex items-center relative rounded-md bg-onboarding-background-200">
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
@ -80,7 +91,7 @@ export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder="orville.wright@firstflight.com"
|
placeholder="orville.wright@firstflight.com"
|
||||||
className={`w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12`}
|
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
||||||
/>
|
/>
|
||||||
{value.length > 0 && (
|
{value.length > 0 && (
|
||||||
<XCircle
|
<XCircle
|
||||||
@ -91,11 +102,28 @@ export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="password"
|
||||||
|
rules={{
|
||||||
|
required: "Password is required",
|
||||||
|
}}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
hasError={Boolean(errors.password)}
|
||||||
|
placeholder="Enter password"
|
||||||
|
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<p className="text-xs text-custom-text-200 pb-2">
|
<p className="text-xs text-custom-text-200 pb-2">
|
||||||
Use your email address if you are the instance admin. <br /> Use your admin’s e-mail if you are not.
|
Use your email address if you are the instance admin. <br /> Use your admin’s e-mail if you are not.
|
||||||
</p>
|
</p>
|
||||||
<Button variant="primary" className="w-full" size="xl" type="submit" loading={isSubmitting}>
|
<Button variant="primary" className="w-full" size="xl" type="submit" loading={isSubmitting}>
|
||||||
{isSubmitting ? "Sending code..." : "Send unique code"}
|
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@ -1,9 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import { Lightbulb } from "lucide-react";
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
@ -14,7 +11,6 @@ import { SignInRoot } from "components/account";
|
|||||||
import { Loader, Spinner } from "@plane/ui";
|
import { Loader, Spinner } from "@plane/ui";
|
||||||
// images
|
// images
|
||||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
import latestFeatures from "public/onboarding/onboarding-pages.svg";
|
|
||||||
|
|
||||||
export type AuthType = "sign-in" | "sign-up";
|
export type AuthType = "sign-in" | "sign-up";
|
||||||
|
|
||||||
@ -24,8 +20,6 @@ export const SignInView = observer(() => {
|
|||||||
user: { currentUser },
|
user: { currentUser },
|
||||||
appConfig: { envConfig },
|
appConfig: { envConfig },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// next-themes
|
|
||||||
const { resolvedTheme } = useTheme();
|
|
||||||
// sign in redirection hook
|
// sign in redirection hook
|
||||||
const { isRedirecting, handleRedirection } = useSignInRedirection();
|
const { isRedirecting, handleRedirection } = useSignInRedirection();
|
||||||
|
|
||||||
@ -66,30 +60,7 @@ export const SignInView = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<SignInRoot />
|
||||||
<SignInRoot />
|
|
||||||
|
|
||||||
<div className="flex py-2 bg-onboarding-background-100 border border-onboarding-border-200 mx-auto rounded-[3.5px] sm:w-96 mt-16">
|
|
||||||
<Lightbulb className="h-7 w-7 mr-2 mx-3" />
|
|
||||||
<p className="text-sm text-left text-onboarding-text-100">
|
|
||||||
Pages gets a facelift! Write anything and use Galileo to help you start.{" "}
|
|
||||||
<Link href="https://plane.so/changelog" target="_blank" rel="noopener noreferrer">
|
|
||||||
<span className="font-medium text-sm underline hover:cursor-pointer">Learn more</span>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="border border-onboarding-border-200 sm:w-96 sm:h-52 object-cover mt-8 mx-auto rounded-md bg-onboarding-background-100 overflow-hidden">
|
|
||||||
<div className="h-[90%]">
|
|
||||||
<Image
|
|
||||||
src={latestFeatures}
|
|
||||||
alt="Plane Issues"
|
|
||||||
className={`rounded-md h-full ml-8 -mt-2 ${
|
|
||||||
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
|
||||||
} `}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, ReactNode, useEffect } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ type Props = {
|
|||||||
const InstanceLayout: FC<Props> = observer(({ children }) => {
|
const InstanceLayout: FC<Props> = observer(({ children }) => {
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
instance: { fetchInstanceInfo, instance, createInstance },
|
instance: { fetchInstanceInfo, instance },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -28,12 +28,6 @@ const InstanceLayout: FC<Props> = observer(({ children }) => {
|
|||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (instance?.is_activated === false) {
|
|
||||||
createInstance();
|
|
||||||
}
|
|
||||||
}, [instance?.is_activated, createInstance]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full overflow-hidden">
|
<div className="h-screen w-full overflow-hidden">
|
||||||
{instance ? (
|
{instance ? (
|
||||||
|
@ -109,7 +109,7 @@ const HomePage: NextPageWithLayout = () => {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.email)}
|
hasError={Boolean(errors.email)}
|
||||||
placeholder="orville.wright@firstflight.com"
|
placeholder="orville.wright@firstflight.com"
|
||||||
className="w-full h-[46px] text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
className="w-full h-[46px] text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -128,7 +128,7 @@ const HomePage: NextPageWithLayout = () => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
hasError={Boolean(errors.password)}
|
hasError={Boolean(errors.password)}
|
||||||
placeholder="Choose password"
|
placeholder="Choose password"
|
||||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||||
minLength={8}
|
minLength={8}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 2.6 MiB |
@ -3,16 +3,20 @@ import { APIService } from "services/api.service";
|
|||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { IEmailCheckData, ILoginTokenResponse, IMagicSignInData, IPasswordSignInData } from "types/auth";
|
import {
|
||||||
|
IEmailCheckData,
|
||||||
|
IEmailCheckResponse,
|
||||||
|
ILoginTokenResponse,
|
||||||
|
IMagicSignInData,
|
||||||
|
IPasswordSignInData,
|
||||||
|
} from "types/auth";
|
||||||
|
|
||||||
export class AuthService extends APIService {
|
export class AuthService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async emailCheck(data: IEmailCheckData): Promise<{
|
async emailCheck(data: IEmailCheckData): Promise<IEmailCheckResponse> {
|
||||||
is_password_autoset: boolean;
|
|
||||||
}> {
|
|
||||||
return this.post("/api/email-check/", data, { headers: {} })
|
return this.post("/api/email-check/", data, { headers: {} })
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -80,18 +84,6 @@ export class AuthService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInstanceAdminPassword(data: any): Promise<any> {
|
|
||||||
return this.post("/api/licenses/instances/admins/set-password/", data)
|
|
||||||
.then((response) => {
|
|
||||||
this.setAccessToken(response?.data?.access_token);
|
|
||||||
this.setRefreshToken(response?.data?.refresh_token);
|
|
||||||
return response?.data;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async socialAuth(data: any): Promise<ILoginTokenResponse> {
|
async socialAuth(data: any): Promise<ILoginTokenResponse> {
|
||||||
return this.post("/api/social-auth/", data, { headers: {} })
|
return this.post("/api/social-auth/", data, { headers: {} })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -104,7 +96,7 @@ export class AuthService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async emailCode(data: any): Promise<any> {
|
async generateUniqueCode(data: { email: string }): Promise<any> {
|
||||||
return this.post("/api/magic-generate/", data, { headers: {} })
|
return this.post("/api/magic-generate/", data, { headers: {} })
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -112,14 +104,6 @@ export class AuthService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async instanceAdminEmailCode(data: any): Promise<any> {
|
|
||||||
return this.post("/api/licenses/instances/admins/magic-generate/", data, { headers: {} })
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async magicSignIn(data: IMagicSignInData): Promise<any> {
|
async magicSignIn(data: IMagicSignInData): Promise<any> {
|
||||||
return await this.post("/api/magic-sign-in/", data, { headers: {} })
|
return await this.post("/api/magic-sign-in/", data, { headers: {} })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -134,14 +118,18 @@ export class AuthService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async instanceMagicSignIn(data: any): Promise<any> {
|
async instanceAdminSignIn(data: IPasswordSignInData): Promise<ILoginTokenResponse> {
|
||||||
const response = await this.post("/api/licenses/instances/admins/magic-sign-in/", data, { headers: {} });
|
return await this.post("/api/licenses/instances/admins/sign-in/", data, { headers: {} })
|
||||||
if (response?.status === 200) {
|
.then((response) => {
|
||||||
this.setAccessToken(response?.data?.access_token);
|
if (response?.status === 200) {
|
||||||
this.setRefreshToken(response?.data?.refresh_token);
|
this.setAccessToken(response?.data?.access_token);
|
||||||
return response?.data;
|
this.setRefreshToken(response?.data?.refresh_token);
|
||||||
}
|
return response?.data;
|
||||||
throw response.response.data;
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async signOut(): Promise<any> {
|
async signOut(): Promise<any> {
|
||||||
|
@ -22,14 +22,6 @@ export class InstanceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createInstance(): Promise<IInstance> {
|
|
||||||
return this.post("/api/licenses/instances/", {}, { headers: {} })
|
|
||||||
.then((response) => response.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getInstanceAdmins(): Promise<IInstanceAdmin[]> {
|
async getInstanceAdmins(): Promise<IInstanceAdmin[]> {
|
||||||
return this.get("/api/licenses/instances/admins/")
|
return this.get("/api/licenses/instances/admins/")
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
|
@ -17,7 +17,6 @@ export interface IInstanceStore {
|
|||||||
formattedConfig: IFormattedInstanceConfiguration | null;
|
formattedConfig: IFormattedInstanceConfiguration | null;
|
||||||
// action
|
// action
|
||||||
fetchInstanceInfo: () => Promise<IInstance>;
|
fetchInstanceInfo: () => Promise<IInstance>;
|
||||||
createInstance: () => Promise<IInstance>;
|
|
||||||
fetchInstanceAdmins: () => Promise<IInstanceAdmin[]>;
|
fetchInstanceAdmins: () => Promise<IInstanceAdmin[]>;
|
||||||
updateInstanceInfo: (data: Partial<IInstance>) => Promise<IInstance>;
|
updateInstanceInfo: (data: Partial<IInstance>) => Promise<IInstance>;
|
||||||
fetchInstanceConfigurations: () => Promise<any>;
|
fetchInstanceConfigurations: () => Promise<any>;
|
||||||
@ -46,7 +45,6 @@ export class InstanceStore implements IInstanceStore {
|
|||||||
formattedConfig: computed,
|
formattedConfig: computed,
|
||||||
// actions
|
// actions
|
||||||
fetchInstanceInfo: action,
|
fetchInstanceInfo: action,
|
||||||
createInstance: action,
|
|
||||||
fetchInstanceAdmins: action,
|
fetchInstanceAdmins: action,
|
||||||
updateInstanceInfo: action,
|
updateInstanceInfo: action,
|
||||||
fetchInstanceConfigurations: action,
|
fetchInstanceConfigurations: action,
|
||||||
@ -86,22 +84,6 @@ export class InstanceStore implements IInstanceStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Creating new Instance In case of no instance found
|
|
||||||
*/
|
|
||||||
createInstance = async () => {
|
|
||||||
try {
|
|
||||||
const instance = await this.instanceService.createInstance();
|
|
||||||
runInAction(() => {
|
|
||||||
this.instance = instance;
|
|
||||||
});
|
|
||||||
return instance;
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error while creating the instance");
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fetch instance admins from API
|
* fetch instance admins from API
|
||||||
*/
|
*/
|
||||||
|
1
web/types/app.d.ts
vendored
1
web/types/app.d.ts
vendored
@ -14,4 +14,5 @@ export interface IAppConfig {
|
|||||||
posthog_host: string | null;
|
posthog_host: string | null;
|
||||||
has_openai_configured: boolean;
|
has_openai_configured: boolean;
|
||||||
has_unsplash_configured: boolean;
|
has_unsplash_configured: boolean;
|
||||||
|
is_self_managed: boolean;
|
||||||
}
|
}
|
||||||
|
8
web/types/auth.d.ts
vendored
8
web/types/auth.d.ts
vendored
@ -2,12 +2,16 @@ export type TEmailCheckTypes = "magic_code" | "password";
|
|||||||
|
|
||||||
export interface IEmailCheckData {
|
export interface IEmailCheckData {
|
||||||
email: string;
|
email: string;
|
||||||
type: TEmailCheckTypes;
|
}
|
||||||
|
|
||||||
|
export interface IEmailCheckResponse {
|
||||||
|
is_password_autoset: boolean;
|
||||||
|
is_existing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILoginTokenResponse {
|
export interface ILoginTokenResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
refresh_toke: string;
|
refresh_token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMagicSignInData {
|
export interface IMagicSignInData {
|
||||||
|
Loading…
Reference in New Issue
Block a user