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
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicGenerateEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
OauthEndpoint,
|
||||
EmailCheckEndpoint,
|
||||
@ -30,6 +31,7 @@ urlpatterns = [
|
||||
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||
# magic sign in
|
||||
path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"),
|
||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
# Password Manipulation
|
||||
|
@ -87,6 +87,7 @@ from .auth_extended import (
|
||||
ChangePasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
EmailCheckEndpoint,
|
||||
MagicGenerateEndpoint,
|
||||
)
|
||||
|
||||
|
||||
|
@ -37,9 +37,9 @@ from plane.bgtasks.forgot_password_task import forgot_password
|
||||
from plane.license.models import Instance, InstanceConfiguration
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return (
|
||||
@ -108,13 +108,16 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
return Response({"error": "Please enter a valid email"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"error": "Please enter a valid email"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the user
|
||||
user = User.objects.filter(email=email).first()
|
||||
if user:
|
||||
# Get the reset token for user
|
||||
uidb64, token = get_tokens_for_user(user=user)
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
# send the forgot password email
|
||||
forgot_password.delay(
|
||||
@ -130,7 +133,9 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class ResetPasswordEndpoint(BaseAPIView):
|
||||
permission_classes = [AllowAny,]
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request, uidb64, token):
|
||||
try:
|
||||
@ -219,6 +224,89 @@ class SetUserPasswordEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class MagicGenerateEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Check the instance registration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
return Response(
|
||||
{"error": "Instance is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not email:
|
||||
return Response(
|
||||
{"error": "Please provide a valid email address"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Clean up the email
|
||||
email = email.strip().lower()
|
||||
validate_email(email)
|
||||
|
||||
# check if the email exists not
|
||||
if not User.objects.filter(email=email).exists():
|
||||
# Create a user
|
||||
_ = User.objects.create(
|
||||
email=email,
|
||||
username=uuid.uuid4().hex,
|
||||
password=make_password(uuid.uuid4().hex),
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
||||
## Generate a random token
|
||||
token = (
|
||||
"".join(random.choices(string.ascii_lowercase, k=4))
|
||||
+ "-"
|
||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
||||
+ "-"
|
||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
||||
)
|
||||
|
||||
ri = redis_instance()
|
||||
|
||||
key = "magic_" + str(email)
|
||||
|
||||
# Check if the key already exists in python
|
||||
if ri.exists(key):
|
||||
data = json.loads(ri.get(key))
|
||||
|
||||
current_attempt = data["current_attempt"] + 1
|
||||
|
||||
if data["current_attempt"] > 2:
|
||||
return Response(
|
||||
{"error": "Max attempts exhausted. Please try again later."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
value = {
|
||||
"current_attempt": current_attempt,
|
||||
"email": email,
|
||||
"token": token,
|
||||
}
|
||||
expiry = 600
|
||||
|
||||
ri.set(key, json.dumps(value), ex=expiry)
|
||||
|
||||
else:
|
||||
value = {"current_attempt": 0, "email": email, "token": token}
|
||||
expiry = 600
|
||||
|
||||
ri.set(key, json.dumps(value), ex=expiry)
|
||||
|
||||
# If the smtp is configured send through here
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
magic_link.delay(email, key, token, current_site)
|
||||
|
||||
return Response({"key": key}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class EmailCheckEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
@ -237,16 +325,19 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
|
||||
email = request.data.get("email", False)
|
||||
type = request.data.get("type", "magic_code")
|
||||
|
||||
if not email:
|
||||
return Response({"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# validate the email
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
return Response({"error": "Email is not valid"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"error": "Email is not valid"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Check if the user exists
|
||||
user = User.objects.filter(email=email).first()
|
||||
@ -281,71 +372,59 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
||||
# Update instance user count
|
||||
update_user_instance_user_count.delay()
|
||||
|
||||
# Case when the user selects magic code
|
||||
if type == "magic_code":
|
||||
if not bool(get_configuration_value(
|
||||
if not bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN")),
|
||||
):
|
||||
return Response(
|
||||
{"error": "Magic link sign in is disabled."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=True,
|
||||
)
|
||||
key, token, current_attempt = generate_magic_token(email=email)
|
||||
if not current_attempt:
|
||||
return Response({"error": "Max attempts exhausted. Please try again later."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# Trigger the email
|
||||
magic_link.delay(email, "magic_" + str(email), token, current_site)
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
# Get the uidb64 and token for the user
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
||||
),
|
||||
):
|
||||
return Response(
|
||||
{"error": "Magic link sign in is disabled."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=True,
|
||||
)
|
||||
# Automatically send the email
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=True,
|
||||
)
|
||||
key, token, current_attempt = generate_magic_token(email=email)
|
||||
if not current_attempt:
|
||||
return Response(
|
||||
{"error": "Max attempts exhausted. Please try again later."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# Trigger the email
|
||||
magic_link.delay(email, "magic_" + str(email), token, current_site)
|
||||
return Response(
|
||||
{"is_password_autoset": user.is_password_autoset, "is_existing": False},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
# Existing user
|
||||
else:
|
||||
if type == "magic_code":
|
||||
if user.is_password_autoset:
|
||||
## Generate a random token
|
||||
if not bool(get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN")),
|
||||
if not bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
||||
),
|
||||
):
|
||||
return Response(
|
||||
{"error": "Magic link sign in is disabled."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
@ -356,15 +435,24 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
medium="MAGIC_LINK",
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
|
||||
# Generate magic token
|
||||
key, token, current_attempt = generate_magic_token(email=email)
|
||||
if not current_attempt:
|
||||
return Response({"error": "Max attempts exhausted. Please try again later."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"error": "Max attempts exhausted. Please try again later."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Trigger the email
|
||||
magic_link.delay(email, key, token, current_site)
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
{
|
||||
"is_password_autoset": user.is_password_autoset,
|
||||
"is_existing": True,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
else:
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
@ -376,14 +464,12 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
medium="EMAIL",
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
if user.is_password_autoset:
|
||||
# send email
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
)
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
# User should enter password to login
|
||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||
|
||||
# User should enter password to login
|
||||
return Response(
|
||||
{
|
||||
"is_password_autoset": user.is_password_autoset,
|
||||
"is_existing": True,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
@ -1,8 +1,6 @@
|
||||
# Python imports
|
||||
import os
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
@ -10,6 +8,7 @@ from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
@ -31,7 +30,6 @@ from plane.settings.redis import redis_instance
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
||||
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
@ -58,7 +56,6 @@ class SignUpEndpoint(BaseAPIView):
|
||||
|
||||
email = request.data.get("email", False)
|
||||
password = request.data.get("password", False)
|
||||
|
||||
## Raise exception if any of the above are missing
|
||||
if not email or not password:
|
||||
return Response(
|
||||
@ -66,8 +63,8 @@ class SignUpEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate the email
|
||||
email = email.strip().lower()
|
||||
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError as e:
|
||||
@ -106,6 +103,7 @@ class SignUpEndpoint(BaseAPIView):
|
||||
user.set_password(password)
|
||||
|
||||
# settings last actives for the user
|
||||
user.is_password_autoset = False
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
@ -120,9 +118,6 @@ class SignUpEndpoint(BaseAPIView):
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
|
||||
# Update instance user count
|
||||
update_user_instance_user_count.delay()
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@ -148,8 +143,8 @@ class SignInEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate email
|
||||
email = email.strip().lower()
|
||||
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError as e:
|
||||
@ -161,22 +156,45 @@ class SignInEndpoint(BaseAPIView):
|
||||
# Get the user
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# User is not present in db
|
||||
if user is None:
|
||||
return Response(
|
||||
{
|
||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# Existing user
|
||||
if user:
|
||||
# Check user password
|
||||
if not user.check_password(password):
|
||||
return Response(
|
||||
{
|
||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Check user password
|
||||
if not user.check_password(password):
|
||||
return Response(
|
||||
{
|
||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
# Create the user
|
||||
else:
|
||||
# Get the configurations
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
# Create the user
|
||||
if (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
== "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=email,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
user = User.objects.create(
|
||||
email=email,
|
||||
username=uuid.uuid4().hex,
|
||||
password=make_password(password),
|
||||
is_password_autoset=False,
|
||||
)
|
||||
|
||||
# settings last active for the user
|
||||
|
@ -11,7 +11,7 @@ from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.license.models import Instance, InstanceConfiguration
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
@ -104,4 +104,6 @@ class ConfigurationEndpoint(BaseAPIView):
|
||||
|
||||
data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||
|
||||
data["is_self_managed"] = bool(int(os.environ.get("IS_SELF_MANAGED", "1")))
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
@ -32,7 +32,6 @@ from plane.bgtasks.event_tracking_task import auth_events
|
||||
from .base import BaseAPIView
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
||||
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
@ -439,6 +438,4 @@ class OauthEndpoint(BaseAPIView):
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
|
||||
# Update the user count
|
||||
update_user_instance_user_count.delay()
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
|
@ -66,30 +66,6 @@ def send_export_email(email, slug, csv_buffer, rows):
|
||||
EMAIL_FROM,
|
||||
) = 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(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
|
@ -39,32 +39,6 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
EMAIL_FROM,
|
||||
) = 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"
|
||||
|
||||
context = {
|
||||
|
@ -26,7 +26,6 @@ from plane.db.models import (
|
||||
IssueProperty,
|
||||
)
|
||||
from plane.bgtasks.user_welcome_task import send_welcome_slack
|
||||
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
||||
|
||||
|
||||
@shared_task
|
||||
@ -121,9 +120,6 @@ def service_importer(service, importer_id):
|
||||
batch_size=100,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Update instance user count
|
||||
update_user_instance_user_count.delay()
|
||||
|
||||
# Check if sync config is on for github importers
|
||||
if service == "github" and importer.config.get("sync", False):
|
||||
|
@ -34,30 +34,6 @@ def magic_link(email, key, token, current_site):
|
||||
EMAIL_FROM,
|
||||
) = 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
|
||||
subject = f"Your unique Plane login code is {token}"
|
||||
context = {"code": token, "email": email}
|
||||
|
@ -14,7 +14,7 @@ from sentry_sdk import capture_exception
|
||||
# Module imports
|
||||
from plane.db.models import Project, User, ProjectMemberInvite
|
||||
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
|
||||
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
|
||||
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(
|
||||
host=get_configuration_value(
|
||||
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST")
|
||||
),
|
||||
port=int(
|
||||
get_configuration_value(
|
||||
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"),
|
||||
)
|
||||
),
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_FROM",
|
||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
||||
),
|
||||
from_email=EMAIL_FROM,
|
||||
to=[email],
|
||||
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,
|
||||
) = 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 = 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",
|
||||
"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.
|
||||
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):
|
||||
data = super().to_representation(instance)
|
||||
# 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)
|
||||
|
||||
return data
|
||||
return data
|
||||
|
@ -2,8 +2,6 @@ from .instance import (
|
||||
InstanceEndpoint,
|
||||
InstanceAdminEndpoint,
|
||||
InstanceConfigurationEndpoint,
|
||||
AdminSetupMagicSignInEndpoint,
|
||||
InstanceAdminSignInEndpoint,
|
||||
SignUpScreenVisitedEndpoint,
|
||||
AdminMagicSignInGenerateEndpoint,
|
||||
AdminSetUserPasswordEndpoint,
|
||||
)
|
||||
|
@ -27,14 +27,11 @@ from plane.license.api.serializers import (
|
||||
InstanceAdminSerializer,
|
||||
InstanceConfigurationSerializer,
|
||||
)
|
||||
from plane.app.serializers import UserSerializer
|
||||
from plane.license.api.permissions import (
|
||||
InstanceAdminPermission,
|
||||
)
|
||||
from plane.db.models import User
|
||||
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):
|
||||
@ -47,61 +44,6 @@ class InstanceEndpoint(BaseAPIView):
|
||||
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):
|
||||
instance = Instance.objects.first()
|
||||
# get the instance
|
||||
@ -122,24 +64,6 @@ class InstanceEndpoint(BaseAPIView):
|
||||
serializer = InstanceSerializer(instance, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
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.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -212,12 +136,7 @@ class InstanceConfigurationEndpoint(BaseAPIView):
|
||||
bulk_configurations = []
|
||||
for configuration in configurations:
|
||||
value = request.data.get(configuration.key, configuration.value)
|
||||
if value is not None and configuration.key in [
|
||||
"OPENAI_API_KEY",
|
||||
"GITHUB_CLIENT_SECRET",
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
"UNSPLASH_ACESS_KEY",
|
||||
]:
|
||||
if configuration.is_encrypted:
|
||||
configuration.value = encrypt_data(value)
|
||||
else:
|
||||
configuration.value = value
|
||||
@ -239,15 +158,13 @@ def get_tokens_for_user(user):
|
||||
)
|
||||
|
||||
|
||||
class AdminMagicSignInGenerateEndpoint(BaseAPIView):
|
||||
class InstanceAdminSignInEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Check the instance registration
|
||||
# Check instance first
|
||||
instance = Instance.objects.first()
|
||||
if instance is None:
|
||||
return Response(
|
||||
@ -255,193 +172,63 @@ class AdminMagicSignInGenerateEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# check if the instance is already activated
|
||||
if InstanceAdmin.objects.first():
|
||||
return Response(
|
||||
{"error": "Admin for this instance is already registered"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not email:
|
||||
return Response(
|
||||
{"error": "Please provide a valid email address"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Clean up
|
||||
email = email.strip().lower()
|
||||
validate_email(email)
|
||||
|
||||
# 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)
|
||||
# Get the email and password from all the user
|
||||
email = request.data.get("email", False)
|
||||
password = request.data.get("password", False)
|
||||
|
||||
# If the user password is not autoset then return error
|
||||
if not user.is_password_autoset:
|
||||
# return error if the email and password is not present
|
||||
if not email or not password:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your password is already set please change your password from profile"
|
||||
},
|
||||
{"error": "Email and password are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check password validation
|
||||
if not password and len(str(password)) < 8:
|
||||
# Validate the email
|
||||
email = email.strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError as e:
|
||||
return Response(
|
||||
{"error": "Password is not valid"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
instance = Instance.objects.first()
|
||||
if instance is None:
|
||||
return Response(
|
||||
{"error": "Instance is not configured"},
|
||||
{"error": "Please provide a valid email address."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Save the user in control center
|
||||
headers = {
|
||||
"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}),
|
||||
)
|
||||
# Check if already a user exists or not
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# Also register the user as admin
|
||||
_ = requests.post(
|
||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/register/",
|
||||
headers=headers,
|
||||
data=json.dumps(
|
||||
{
|
||||
"email": str(user.email),
|
||||
"signup_mode": "MAGIC_CODE",
|
||||
"is_admin": True,
|
||||
}
|
||||
),
|
||||
)
|
||||
# Existing user
|
||||
if user:
|
||||
# Check user password
|
||||
if not user.check_password(password):
|
||||
return Response(
|
||||
{
|
||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
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
|
||||
_ = InstanceAdmin.objects.create(
|
||||
@ -452,12 +239,13 @@ class AdminSetUserPasswordEndpoint(BaseAPIView):
|
||||
instance.is_setup_done = True
|
||||
instance.save()
|
||||
|
||||
# Set the user password
|
||||
user.set_password(password)
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
serializer = UserSerializer(user)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
# get tokens for user
|
||||
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)
|
||||
|
||||
|
||||
class SignUpScreenVisitedEndpoint(BaseAPIView):
|
||||
@ -467,27 +255,11 @@ class SignUpScreenVisitedEndpoint(BaseAPIView):
|
||||
|
||||
def post(self, request):
|
||||
instance = Instance.objects.first()
|
||||
|
||||
if instance is None:
|
||||
return Response(
|
||||
{"error": "Instance is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not instance.is_signup_screen_visited:
|
||||
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),
|
||||
)
|
||||
instance.is_signup_screen_visited = True
|
||||
instance.save()
|
||||
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",
|
||||
"value": os.environ.get("ENABLE_SIGNUP", "1"),
|
||||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_EMAIL_PASSWORD",
|
||||
"value": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
||||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"value": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"),
|
||||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GOOGLE_CLIENT_ID",
|
||||
"value": os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
"category": "GOOGLE",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_ID",
|
||||
"value": os.environ.get("GITHUB_CLIENT_ID"),
|
||||
"category": "GITHUB",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_SECRET",
|
||||
"value": encrypt_data(os.environ.get("GITHUB_CLIENT_SECRET"))
|
||||
if os.environ.get("GITHUB_CLIENT_SECRET")
|
||||
else None,
|
||||
"value": os.environ.get("GITHUB_CLIENT_SECRET"),
|
||||
"category": "GITHUB",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"value": os.environ.get("EMAIL_HOST", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_USER",
|
||||
"value": os.environ.get("EMAIL_HOST_USER", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_PASSWORD",
|
||||
"value": encrypt_data(os.environ.get("EMAIL_HOST_PASSWORD"))
|
||||
if os.environ.get("EMAIL_HOST_PASSWORD")
|
||||
else None,
|
||||
"value": os.environ.get("EMAIL_HOST_PASSWORD", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_PORT",
|
||||
"value": os.environ.get("EMAIL_PORT", "587"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_FROM",
|
||||
"value": os.environ.get("EMAIL_FROM", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_USE_TLS",
|
||||
"value": os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "OPENAI_API_KEY",
|
||||
"value": encrypt_data(os.environ.get("OPENAI_API_KEY"))
|
||||
if os.environ.get("OPENAI_API_KEY")
|
||||
else None,
|
||||
"value": os.environ.get("OPENAI_API_KEY"),
|
||||
"category": "OPENAI",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "GPT_ENGINE",
|
||||
"value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "UNSPLASH_ACCESS_KEY",
|
||||
"value": encrypt_data(os.environ.get("UNSPLASH_ACESS_KEY", ""))
|
||||
if os.environ.get("UNSPLASH_ACESS_KEY")
|
||||
else None,
|
||||
"value": os.environ.get("UNSPLASH_ACESS_KEY", ""),
|
||||
"category": "UNSPLASH",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
]
|
||||
|
||||
@ -107,8 +114,12 @@ class Command(BaseCommand):
|
||||
key=item.get("key")
|
||||
)
|
||||
if created:
|
||||
obj.value = item.get("value")
|
||||
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()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Python imports
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import secrets
|
||||
|
||||
# Django imports
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
@ -31,13 +31,12 @@ class Command(BaseCommand):
|
||||
data = json.load(file)
|
||||
|
||||
machine_signature = options.get("machine_signature", False)
|
||||
|
||||
|
||||
if not machine_signature:
|
||||
raise CommandError("Machine signature is required")
|
||||
|
||||
# Check if machine is online
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
payload = {
|
||||
"instance_key": settings.INSTANCE_KEY,
|
||||
"version": data.get("version", 0.1),
|
||||
@ -45,25 +44,44 @@ class Command(BaseCommand):
|
||||
"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),
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||
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),
|
||||
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.style.SUCCESS(
|
||||
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.db import migrations, models
|
||||
@ -34,6 +34,7 @@ class Migration(migrations.Migration):
|
||||
('is_setup_done', models.BooleanField(default=False)),
|
||||
('is_signup_screen_visited', models.BooleanField(default=False)),
|
||||
('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')),
|
||||
('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)),
|
||||
('value', models.TextField(blank=True, default=None, null=True)),
|
||||
('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')),
|
||||
('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')),
|
||||
('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)),
|
||||
('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')),
|
||||
('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')),
|
||||
|
@ -30,6 +30,7 @@ class Instance(BaseModel):
|
||||
is_signup_screen_visited = models.BooleanField(default=False)
|
||||
# users
|
||||
user_count = models.PositiveBigIntegerField(default=0)
|
||||
is_verified = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Instance"
|
||||
@ -47,6 +48,7 @@ class InstanceAdmin(BaseModel):
|
||||
)
|
||||
instance = models.ForeignKey(Instance, on_delete=models.CASCADE, related_name="admins")
|
||||
role = models.PositiveIntegerField(choices=ROLE_CHOICES, default=20)
|
||||
is_verified = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["instance", "user"]
|
||||
@ -61,6 +63,7 @@ class InstanceConfiguration(BaseModel):
|
||||
key = models.CharField(max_length=100, unique=True)
|
||||
value = models.TextField(null=True, blank=True, default=None)
|
||||
category = models.TextField()
|
||||
is_encrypted = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Instance Configuration"
|
||||
|
@ -4,9 +4,7 @@ from plane.license.api.views import (
|
||||
InstanceEndpoint,
|
||||
InstanceAdminEndpoint,
|
||||
InstanceConfigurationEndpoint,
|
||||
AdminMagicSignInGenerateEndpoint,
|
||||
AdminSetupMagicSignInEndpoint,
|
||||
AdminSetUserPasswordEndpoint,
|
||||
InstanceAdminSignInEndpoint,
|
||||
SignUpScreenVisitedEndpoint,
|
||||
)
|
||||
|
||||
@ -32,19 +30,9 @@ urlpatterns = [
|
||||
name="instance-configuration",
|
||||
),
|
||||
path(
|
||||
"instances/admins/magic-generate/",
|
||||
AdminMagicSignInGenerateEndpoint.as_view(),
|
||||
name="instance-admins",
|
||||
),
|
||||
path(
|
||||
"instances/admins/magic-sign-in/",
|
||||
AdminSetupMagicSignInEndpoint.as_view(),
|
||||
name="instance-admins",
|
||||
),
|
||||
path(
|
||||
"instances/admins/set-password/",
|
||||
AdminSetUserPasswordEndpoint.as_view(),
|
||||
name="instance-admins",
|
||||
"instances/admins/sign-in/",
|
||||
InstanceAdminSignInEndpoint.as_view(),
|
||||
name="instance-admin-sign-in",
|
||||
),
|
||||
path(
|
||||
"instances/admins/sign-up-screen-visited/",
|
||||
|
@ -110,7 +110,9 @@ CSRF_COOKIE_SECURE = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
cors_origins_raw = os.environ.get("CORS_ALLOWED_ORIGINS", "")
|
||||
# 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:
|
||||
CORS_ALLOWED_ORIGINS = cors_allowed_origins
|
||||
else:
|
||||
@ -286,6 +288,7 @@ CELERY_IMPORTS = (
|
||||
"plane.bgtasks.issue_automation_task",
|
||||
"plane.bgtasks.exporter_expired_task",
|
||||
"plane.bgtasks.file_asset_task",
|
||||
"plane.license.bgtasks.instance_verification_task",
|
||||
)
|
||||
|
||||
# Sentry Settings
|
||||
@ -327,7 +330,11 @@ POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
|
||||
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
|
||||
|
||||
# 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 = 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);
|
||||
}, []);
|
||||
return (
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<div className="w-full">
|
||||
<Link
|
||||
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;
|
||||
handleStepChange: (step: ESignInSteps) => void;
|
||||
handleSignInRedirection: () => Promise<void>;
|
||||
isOnboarded: boolean;
|
||||
};
|
||||
|
||||
type TCreatePasswordFormValues = {
|
||||
@ -32,7 +33,7 @@ const defaultValues: TCreatePasswordFormValues = {
|
||||
const authService = new AuthService();
|
||||
|
||||
export const CreatePasswordForm: React.FC<Props> = (props) => {
|
||||
const { email, handleSignInRedirection } = props;
|
||||
const { email, handleSignInRedirection, isOnboarded } = props;
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// form info
|
||||
@ -76,9 +77,8 @@ export const CreatePasswordForm: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
|
||||
<form onSubmit={handleSubmit(handleCreatePassword)} className="mt-11 sm:w-96 mx-auto space-y-4">
|
||||
<Controller
|
||||
control={control}
|
||||
@ -97,37 +97,32 @@ export const CreatePasswordForm: React.FC<Props> = (props) => {
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
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
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
rules={{
|
||||
required: "Password is required",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Create password"
|
||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
||||
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>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
rules={{
|
||||
required: "Password is required",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Choose password"
|
||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||
minLength={8}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<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>
|
||||
<p className="text-xs text-onboarding-text-200">
|
||||
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 { XCircle } from "lucide-react";
|
||||
// services
|
||||
@ -10,7 +10,7 @@ import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// types
|
||||
import { IEmailCheckData, TEmailCheckTypes } from "types/auth";
|
||||
import { IEmailCheckData } from "types/auth";
|
||||
// constants
|
||||
import { ESignInSteps } from "components/account";
|
||||
|
||||
@ -19,7 +19,7 @@ type Props = {
|
||||
updateEmail: (email: string) => void;
|
||||
};
|
||||
|
||||
type TEmailCodeFormValues = {
|
||||
type TEmailFormValues = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
@ -27,17 +27,14 @@ const authService = new AuthService();
|
||||
|
||||
export const EmailForm: React.FC<Props> = (props) => {
|
||||
const { handleStepChange, updateEmail } = props;
|
||||
// states
|
||||
const [isCheckingEmail, setIsCheckingEmail] = useState<TEmailCheckTypes | null>(null);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isValid },
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
handleSubmit,
|
||||
watch,
|
||||
} = useForm<TEmailCodeFormValues>({
|
||||
} = useForm<TEmailFormValues>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
@ -45,31 +42,21 @@ export const EmailForm: React.FC<Props> = (props) => {
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const handleEmailCheck = async (type: TEmailCheckTypes) => {
|
||||
setIsCheckingEmail(type);
|
||||
|
||||
const email = watch("email");
|
||||
|
||||
const handleFormSubmit = async (data: TEmailFormValues) => {
|
||||
const payload: IEmailCheckData = {
|
||||
email,
|
||||
type,
|
||||
email: data.email,
|
||||
};
|
||||
|
||||
// update the global email state
|
||||
updateEmail(email);
|
||||
updateEmail(data.email);
|
||||
|
||||
await authService
|
||||
.emailCheck(payload)
|
||||
.then((res) => {
|
||||
// if type is magic_code, send the user to magic sign in
|
||||
if (type === "magic_code") handleStepChange(ESignInSteps.UNIQUE_CODE);
|
||||
// if type is password, check if the user has a password set
|
||||
if (type === "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);
|
||||
}
|
||||
// if the password has been autoset, send the user to magic sign-in
|
||||
if (res.is_password_autoset) handleStepChange(ESignInSteps.UNIQUE_CODE);
|
||||
// if the password has not been autoset, send them to password sign-in
|
||||
else handleStepChange(ESignInSteps.PASSWORD);
|
||||
})
|
||||
.catch((err) =>
|
||||
setToastAlert({
|
||||
@ -77,8 +64,7 @@ export const EmailForm: React.FC<Props> = (props) => {
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setIsCheckingEmail(null));
|
||||
);
|
||||
};
|
||||
|
||||
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">
|
||||
Get on your flight deck
|
||||
</h1>
|
||||
<p className="text-center text-sm text-onboarding-text-200 mt-3">
|
||||
Sign in with the email you used to sign up for Plane
|
||||
<p className="text-center text-sm text-onboarding-text-200 mt-2.5">
|
||||
Create or join a workspace. Start with your e-mail.
|
||||
</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">
|
||||
<Controller
|
||||
control={control}
|
||||
@ -122,30 +108,9 @@ export const EmailForm: React.FC<Props> = (props) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2.5 mt-4">
|
||||
<Button
|
||||
type="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>
|
||||
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
|
||||
Continue
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
@ -2,7 +2,8 @@ export * from "./create-password";
|
||||
export * from "./email-form";
|
||||
export * from "./o-auth-options";
|
||||
export * from "./optional-set-password";
|
||||
export * from "./unique-code";
|
||||
export * from "./password";
|
||||
export * from "./root";
|
||||
export * from "./self-hosted-sign-in";
|
||||
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";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
import { UserService } from "services/user.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { ESignInSteps, GithubLoginButton, GoogleLoginButton } from "components/account";
|
||||
import { GithubLoginButton, GoogleLoginButton } from "components/account";
|
||||
|
||||
type Props = {
|
||||
updateEmail: (email: string) => void;
|
||||
handleStepChange: (step: ESignInSteps) => void;
|
||||
handleSignInRedirection: () => Promise<void>;
|
||||
};
|
||||
|
||||
// services
|
||||
const authService = new AuthService();
|
||||
const userService = new UserService();
|
||||
|
||||
export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||
const { updateEmail, handleStepChange, handleSignInRedirection } = props;
|
||||
const { handleSignInRedirection } = props;
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// mobx store
|
||||
@ -38,14 +34,7 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
const response = await authService.socialAuth(socialAuthPayload);
|
||||
|
||||
if (response) {
|
||||
const currentUser = await userService.currentUser();
|
||||
|
||||
updateEmail(currentUser.email);
|
||||
|
||||
if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD);
|
||||
else handleSignInRedirection();
|
||||
}
|
||||
if (response) handleSignInRedirection();
|
||||
} else throw Error("Cant find credentials");
|
||||
} catch (err: any) {
|
||||
setToastAlert({
|
||||
@ -66,14 +55,7 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
const response = await authService.socialAuth(socialAuthPayload);
|
||||
|
||||
if (response) {
|
||||
const currentUser = await userService.currentUser();
|
||||
|
||||
updateEmail(currentUser.email);
|
||||
|
||||
if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD);
|
||||
else handleSignInRedirection();
|
||||
}
|
||||
if (response) handleSignInRedirection();
|
||||
} else throw Error("Cant find credentials");
|
||||
} catch (err: any) {
|
||||
setToastAlert({
|
||||
@ -87,11 +69,11 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<hr className={`border-onboarding-border-100 w-full`} />
|
||||
<hr className="border-onboarding-border-100 w-full" />
|
||||
</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 && (
|
||||
<GoogleLoginButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
||||
)}
|
||||
|
@ -12,13 +12,14 @@ type Props = {
|
||||
email: string;
|
||||
handleStepChange: (step: ESignInSteps) => void;
|
||||
handleSignInRedirection: () => Promise<void>;
|
||||
isOnboarded: boolean;
|
||||
};
|
||||
|
||||
export const OptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
const { email, handleStepChange, handleSignInRedirection } = props;
|
||||
const { email, handleStepChange, handleSignInRedirection, isOnboarded } = props;
|
||||
// states
|
||||
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
||||
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isValid },
|
||||
@ -39,8 +40,8 @@ export const OptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
If you{"'"}d to do away with codes, set a password here
|
||||
<p className="text-center text-sm text-onboarding-text-200 px-20 mt-2.5">
|
||||
If you{"'"}d like to do away with codes, set a password here.
|
||||
</p>
|
||||
|
||||
<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}
|
||||
loading={isGoingToWorkspace}
|
||||
>
|
||||
{isGoingToWorkspace ? "Going to app..." : "Go to workspace"}
|
||||
{isOnboarded ? "Go to workspace" : "Set up workspace"}
|
||||
</Button>
|
||||
</div>
|
||||
<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">
|
||||
<span className="font-semibold underline">terms and conditions of service.</span>
|
||||
</Link>
|
||||
|
@ -11,7 +11,7 @@ import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// types
|
||||
import { IEmailCheckData, IPasswordSignInData } from "types/auth";
|
||||
import { IPasswordSignInData } from "types/auth";
|
||||
// constants
|
||||
import { ESignInSteps } from "components/account";
|
||||
|
||||
@ -37,6 +37,7 @@ const authService = new AuthService();
|
||||
export const PasswordForm: React.FC<Props> = (props) => {
|
||||
const { email, updateEmail, handleStepChange, handleSignInRedirection } = props;
|
||||
// states
|
||||
const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false);
|
||||
const [isSendingResetPasswordLink, setIsSendingResetPasswordLink] = useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
@ -46,7 +47,6 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
||||
formState: { dirtyFields, errors, isSubmitting, isValid },
|
||||
getValues,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setError,
|
||||
} = useForm<TPasswordFormValues>({
|
||||
defaultValues: {
|
||||
@ -57,7 +57,9 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const handlePasswordSignIn = async (formData: TPasswordFormValues) => {
|
||||
const handleFormSubmit = async (formData: TPasswordFormValues) => {
|
||||
updateEmail(formData.email);
|
||||
|
||||
const payload: IPasswordSignInData = {
|
||||
email: formData.email,
|
||||
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 emailFormValue = getValues("email");
|
||||
|
||||
@ -130,6 +102,31 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
||||
.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 (
|
||||
<>
|
||||
<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"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
updateEmail(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (dirtyFields.email) handleEmailCheck(getValues());
|
||||
}}
|
||||
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"
|
||||
@ -186,7 +177,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
||||
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"
|
||||
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}
|
||||
>
|
||||
{isSendingResetPasswordLink ? "Sending link..." : "Forgot your password?"}
|
||||
{isSendingResetPasswordLink ? "Sending link" : "Forgot your password?"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
|
||||
{isSubmitting ? "Signing in..." : "Go to workspace"}
|
||||
</Button>
|
||||
<div className="grid sm:grid-cols-2 gap-2.5">
|
||||
<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">
|
||||
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">
|
||||
<span className="font-semibold underline">terms and conditions of service.</span>
|
||||
</Link>
|
||||
|
@ -1,7 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||
// components
|
||||
import { LatestFeatureBlock } from "components/common";
|
||||
import {
|
||||
EmailForm,
|
||||
UniqueCodeForm,
|
||||
@ -10,6 +14,7 @@ import {
|
||||
OAuthOptions,
|
||||
OptionalSetPasswordForm,
|
||||
CreatePasswordForm,
|
||||
SelfHostedSignInForm,
|
||||
} from "components/account";
|
||||
|
||||
export enum ESignInSteps {
|
||||
@ -19,64 +24,96 @@ export enum ESignInSteps {
|
||||
UNIQUE_CODE = "UNIQUE_CODE",
|
||||
OPTIONAL_SET_PASSWORD = "OPTIONAL_SET_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];
|
||||
|
||||
export const SignInRoot = () => {
|
||||
export const SignInRoot = observer(() => {
|
||||
// states
|
||||
const [signInStep, setSignInStep] = useState<ESignInSteps>(ESignInSteps.EMAIL);
|
||||
const [email, setEmail] = useState("");
|
||||
const [isOnboarded, setIsOnboarded] = useState(false);
|
||||
// sign in redirection hook
|
||||
const { handleRedirection } = useSignInRedirection();
|
||||
// mobx store
|
||||
const {
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
|
||||
const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto flex flex-col">
|
||||
{signInStep === ESignInSteps.EMAIL && (
|
||||
<EmailForm handleStepChange={(step) => setSignInStep(step)} updateEmail={(newEmail) => setEmail(newEmail)} />
|
||||
)}
|
||||
{signInStep === ESignInSteps.PASSWORD && (
|
||||
<PasswordForm
|
||||
{envConfig?.is_self_managed ? (
|
||||
<SelfHostedSignInForm
|
||||
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.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}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{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>
|
||||
{!OAUTH_HIDDEN_STEPS.includes(signInStep) && (
|
||||
<OAuthOptions
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
/>
|
||||
{isOAuthEnabled && !OAUTH_HIDDEN_STEPS.includes(signInStep) && (
|
||||
<OAuthOptions 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 Link from "next/link";
|
||||
import React from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { XCircle } from "lucide-react";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
@ -22,14 +20,12 @@ const authService = new AuthService();
|
||||
|
||||
export const SetPasswordLink: React.FC<Props> = (props) => {
|
||||
const { email, updateEmail } = props;
|
||||
// states
|
||||
const [isSendingNewLink, setIsSendingNewLink] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isValid },
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
handleSubmit,
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
@ -40,17 +36,14 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
||||
});
|
||||
|
||||
const handleSendNewLink = async (formData: { email: string }) => {
|
||||
setIsSendingNewLink(true);
|
||||
|
||||
updateEmail(formData.email);
|
||||
|
||||
const payload: IEmailCheckData = {
|
||||
email: formData.email,
|
||||
type: "password",
|
||||
};
|
||||
|
||||
await authService
|
||||
.emailCheck(payload)
|
||||
.sendResetPasswordLink(payload)
|
||||
.then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@ -64,8 +57,7 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setIsSendingNewLink(false));
|
||||
);
|
||||
};
|
||||
|
||||
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">
|
||||
Get on your flight deck
|
||||
</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
|
||||
password
|
||||
</p>
|
||||
@ -87,45 +79,24 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
||||
required: "Email is required",
|
||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<div className="flex items-center relative rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
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>
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<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] text-onboarding-text-400 border border-onboarding-border-100 pr-12 !bg-onboarding-background-200"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
size="xl"
|
||||
disabled={!isValid}
|
||||
loading={isSendingNewLink}
|
||||
>
|
||||
{isSendingNewLink ? "Sending new link..." : "Get link again"}
|
||||
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
|
||||
{isSubmitting ? "Sending new link" : "Get link again"}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
@ -22,6 +22,9 @@ type Props = {
|
||||
updateEmail: (email: string) => void;
|
||||
handleStepChange: (step: ESignInSteps) => void;
|
||||
handleSignInRedirection: () => Promise<void>;
|
||||
submitButtonLabel?: string;
|
||||
showTermsAndConditions?: boolean;
|
||||
updateUserOnboardingStatus: (value: boolean) => void;
|
||||
};
|
||||
|
||||
type TUniqueCodeFormValues = {
|
||||
@ -39,7 +42,15 @@ const authService = new AuthService();
|
||||
const userService = new UserService();
|
||||
|
||||
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
|
||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||
// toast alert
|
||||
@ -74,6 +85,8 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
.then(async () => {
|
||||
const currentUser = await userService.currentUser();
|
||||
|
||||
updateUserOnboardingStatus(currentUser.is_onboarded);
|
||||
|
||||
if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD);
|
||||
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 = {
|
||||
email: formData.email,
|
||||
type: "magic_code",
|
||||
};
|
||||
|
||||
await authService
|
||||
.emailCheck(payload)
|
||||
.generateUniqueCode(payload)
|
||||
.then(() => {
|
||||
setResendCodeTimer(30);
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
@ -116,25 +129,30 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: TUniqueCodeFormValues) => {
|
||||
if (dirtyFields.email) await handleSendNewLink(formData);
|
||||
updateEmail(formData.email);
|
||||
|
||||
if (dirtyFields.email) await handleSendNewCode(formData);
|
||||
else await handleUniqueCodeSignIn(formData);
|
||||
};
|
||||
|
||||
const handleRequestNewCode = async () => {
|
||||
setIsRequestingNewCode(true);
|
||||
|
||||
await handleSendNewLink(getValues())
|
||||
await handleSendNewCode(getValues())
|
||||
.then(() => setResendCodeTimer(30))
|
||||
.finally(() => setIsRequestingNewCode(false));
|
||||
};
|
||||
|
||||
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
|
||||
const hasEmailChanged = dirtyFields.email;
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
Paste the code you got at <span className="font-semibold text-custom-primary-100">{email}</span> below
|
||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
||||
Get on your flight deck
|
||||
</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>
|
||||
|
||||
<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"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
updateEmail(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
onChange={onChange}
|
||||
onBlur={() => {
|
||||
if (dirtyFields.email) handleSendNewLink(getValues());
|
||||
if (hasEmailChanged) handleSendNewCode(getValues());
|
||||
}}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
@ -174,7 +189,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{dirtyFields.email && (
|
||||
{hasEmailChanged && (
|
||||
<button
|
||||
type="submit"
|
||||
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}
|
||||
name="token"
|
||||
rules={{
|
||||
required: dirtyFields.email ? false : "Code is required",
|
||||
required: hasEmailChanged ? false : "Code is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
@ -196,7 +211,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.token)}
|
||||
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
|
||||
? `Request new code in ${resendTimerCode}s`
|
||||
: isRequestingNewCode
|
||||
? "Requesting new code..."
|
||||
? "Requesting new code"
|
||||
: "Request new code"}
|
||||
</button>
|
||||
</div>
|
||||
@ -224,17 +239,19 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
size="xl"
|
||||
disabled={!isValid || dirtyFields.email}
|
||||
disabled={!isValid || hasEmailChanged}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Signing in..." : "Confirm"}
|
||||
{submitButtonLabel}
|
||||
</Button>
|
||||
<p className="text-xs text-onboarding-text-200">
|
||||
When you click Confirm 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>
|
||||
{showTermsAndConditions && (
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
@ -25,7 +25,7 @@ export const LatestFeatureBlock = () => {
|
||||
<Image
|
||||
src={latestFeatures}
|
||||
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"
|
||||
} `}
|
||||
/>
|
||||
|
@ -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="relative px-7 sm:px-0 bg-onboarding-gradient-200 h-full rounded-t-md">
|
||||
<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 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 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>
|
||||
|
@ -55,7 +55,7 @@ export const InstanceSetupDone = () => {
|
||||
</p>
|
||||
</div>
|
||||
<Button size="lg" prependIcon={<UserCog2 />} onClick={redirectToGodMode} loading={isRedirecting}>
|
||||
{isRedirecting ? "Redirecting..." : "Go to God Mode"}
|
||||
Go to God Mode
|
||||
</Button>
|
||||
</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 "./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";
|
||||
// components
|
||||
import { InstanceSetupEmailCodeForm } from "./email-code-form";
|
||||
import { InstanceSetupEmailForm } from "./email-form";
|
||||
import { InstanceSetupPasswordForm } from "./password-form";
|
||||
import { LatestFeatureBlock } from "components/common";
|
||||
import { InstanceSetupDone } from "components/instance";
|
||||
import { InstanceSetupDone, InstanceSetupSignInForm } from "components/instance";
|
||||
|
||||
export enum EInstanceSetupSteps {
|
||||
EMAIL = "EMAIL",
|
||||
VERIFY_CODE = "VERIFY_CODE",
|
||||
PASSWORD = "PASSWORD",
|
||||
SIGN_IN = "SIGN_IN",
|
||||
DONE = "DONE",
|
||||
}
|
||||
|
||||
export const InstanceSetupFormRoot = () => {
|
||||
// states
|
||||
const [setupStep, setSetupStep] = useState(EInstanceSetupSteps.EMAIL);
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [setupStep, setSetupStep] = useState(EInstanceSetupSteps.SIGN_IN);
|
||||
|
||||
return (
|
||||
<>
|
||||
{setupStep === EInstanceSetupSteps.DONE ? (
|
||||
<InstanceSetupDone />
|
||||
) : (
|
||||
{setupStep === EInstanceSetupSteps.DONE && <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="bg-onboarding-gradient-200 h-full pt-24 pb-56 rounded-t-md overflow-auto">
|
||||
<div className="mx-auto flex flex-col">
|
||||
{setupStep === EInstanceSetupSteps.EMAIL && (
|
||||
<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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<InstanceSetupSignInForm handleNextStep={() => setSetupStep(EInstanceSetupSteps.DONE)} />
|
||||
</div>
|
||||
<LatestFeatureBlock />
|
||||
</div>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Input, Button } from "@plane/ui";
|
||||
// icons
|
||||
@ -9,37 +11,48 @@ import { AuthService } from "services/auth.service";
|
||||
const authService = new AuthService();
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
|
||||
export interface InstanceSetupEmailFormValues {
|
||||
interface InstanceSetupEmailFormValues {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface IInstanceSetupEmailForm {
|
||||
handleNextStep: (email: string) => void;
|
||||
}
|
||||
|
||||
export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
||||
export const InstanceSetupSignInForm: FC<IInstanceSetupEmailForm> = (props) => {
|
||||
const { handleNextStep } = props;
|
||||
const {
|
||||
user: { fetchCurrentUser },
|
||||
} = useMobxStore();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<InstanceSetupEmailFormValues>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleEmailFormSubmit = (formValues: InstanceSetupEmailFormValues) =>
|
||||
authService
|
||||
.instanceAdminEmailCode({ email: formValues.email })
|
||||
.then(() => {
|
||||
reset();
|
||||
const handleFormSubmit = async (formValues: InstanceSetupEmailFormValues) => {
|
||||
const payload = {
|
||||
email: formValues.email,
|
||||
password: formValues.password,
|
||||
};
|
||||
|
||||
await authService
|
||||
.instanceAdminSignIn(payload)
|
||||
.then(async () => {
|
||||
await fetchCurrentUser();
|
||||
handleNextStep(formValues.email);
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -49,9 +62,10 @@ export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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">
|
||||
Let{"'"}s secure your instance
|
||||
</h1>
|
||||
@ -66,13 +80,10 @@ export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
||||
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",
|
||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||
}}
|
||||
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
|
||||
id="email"
|
||||
name="email"
|
||||
@ -80,7 +91,7 @@ export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
||||
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`}
|
||||
className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12"
|
||||
/>
|
||||
{value.length > 0 && (
|
||||
<XCircle
|
||||
@ -91,11 +102,28 @@ export const InstanceSetupEmailForm: FC<IInstanceSetupEmailForm> = (props) => {
|
||||
</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">
|
||||
Use your email address if you are the instance admin. <br /> Use your admin’s e-mail if you are not.
|
||||
</p>
|
||||
<Button variant="primary" className="w-full" size="xl" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Sending code..." : "Send unique code"}
|
||||
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
@ -1,9 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Lightbulb } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
@ -14,7 +11,6 @@ import { SignInRoot } from "components/account";
|
||||
import { Loader, Spinner } from "@plane/ui";
|
||||
// images
|
||||
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";
|
||||
|
||||
@ -24,8 +20,6 @@ export const SignInView = observer(() => {
|
||||
user: { currentUser },
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
// next-themes
|
||||
const { resolvedTheme } = useTheme();
|
||||
// sign in redirection hook
|
||||
const { isRedirecting, handleRedirection } = useSignInRedirection();
|
||||
|
||||
@ -66,30 +60,7 @@ export const SignInView = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
<SignInRoot />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FC, ReactNode, useEffect } from "react";
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
@ -18,7 +18,7 @@ type Props = {
|
||||
const InstanceLayout: FC<Props> = observer(({ children }) => {
|
||||
// store
|
||||
const {
|
||||
instance: { fetchInstanceInfo, instance, createInstance },
|
||||
instance: { fetchInstanceInfo, instance },
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
@ -28,12 +28,6 @@ const InstanceLayout: FC<Props> = observer(({ children }) => {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (instance?.is_activated === false) {
|
||||
createInstance();
|
||||
}
|
||||
}, [instance?.is_activated, createInstance]);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full overflow-hidden">
|
||||
{instance ? (
|
||||
|
@ -109,7 +109,7 @@ const HomePage: NextPageWithLayout = () => {
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
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
|
||||
/>
|
||||
)}
|
||||
@ -128,7 +128,7 @@ const HomePage: NextPageWithLayout = () => {
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.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}
|
||||
/>
|
||||
)}
|
||||
|
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
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import { IEmailCheckData, ILoginTokenResponse, IMagicSignInData, IPasswordSignInData } from "types/auth";
|
||||
import {
|
||||
IEmailCheckData,
|
||||
IEmailCheckResponse,
|
||||
ILoginTokenResponse,
|
||||
IMagicSignInData,
|
||||
IPasswordSignInData,
|
||||
} from "types/auth";
|
||||
|
||||
export class AuthService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async emailCheck(data: IEmailCheckData): Promise<{
|
||||
is_password_autoset: boolean;
|
||||
}> {
|
||||
async emailCheck(data: IEmailCheckData): Promise<IEmailCheckResponse> {
|
||||
return this.post("/api/email-check/", data, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
.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> {
|
||||
return this.post("/api/social-auth/", data, { headers: {} })
|
||||
.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: {} })
|
||||
.then((response) => response?.data)
|
||||
.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> {
|
||||
return await this.post("/api/magic-sign-in/", data, { headers: {} })
|
||||
.then((response) => {
|
||||
@ -134,14 +118,18 @@ export class AuthService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async instanceMagicSignIn(data: any): Promise<any> {
|
||||
const response = await this.post("/api/licenses/instances/admins/magic-sign-in/", data, { headers: {} });
|
||||
if (response?.status === 200) {
|
||||
this.setAccessToken(response?.data?.access_token);
|
||||
this.setRefreshToken(response?.data?.refresh_token);
|
||||
return response?.data;
|
||||
}
|
||||
throw response.response.data;
|
||||
async instanceAdminSignIn(data: IPasswordSignInData): Promise<ILoginTokenResponse> {
|
||||
return await this.post("/api/licenses/instances/admins/sign-in/", data, { headers: {} })
|
||||
.then((response) => {
|
||||
if (response?.status === 200) {
|
||||
this.setAccessToken(response?.data?.access_token);
|
||||
this.setRefreshToken(response?.data?.refresh_token);
|
||||
return response?.data;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
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[]> {
|
||||
return this.get("/api/licenses/instances/admins/")
|
||||
.then((response) => response.data)
|
||||
|
@ -17,7 +17,6 @@ export interface IInstanceStore {
|
||||
formattedConfig: IFormattedInstanceConfiguration | null;
|
||||
// action
|
||||
fetchInstanceInfo: () => Promise<IInstance>;
|
||||
createInstance: () => Promise<IInstance>;
|
||||
fetchInstanceAdmins: () => Promise<IInstanceAdmin[]>;
|
||||
updateInstanceInfo: (data: Partial<IInstance>) => Promise<IInstance>;
|
||||
fetchInstanceConfigurations: () => Promise<any>;
|
||||
@ -46,7 +45,6 @@ export class InstanceStore implements IInstanceStore {
|
||||
formattedConfig: computed,
|
||||
// actions
|
||||
fetchInstanceInfo: action,
|
||||
createInstance: action,
|
||||
fetchInstanceAdmins: action,
|
||||
updateInstanceInfo: 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
|
||||
*/
|
||||
|
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;
|
||||
has_openai_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 {
|
||||
email: string;
|
||||
type: TEmailCheckTypes;
|
||||
}
|
||||
|
||||
export interface IEmailCheckResponse {
|
||||
is_password_autoset: boolean;
|
||||
is_existing: boolean;
|
||||
}
|
||||
|
||||
export interface ILoginTokenResponse {
|
||||
access_token: string;
|
||||
refresh_toke: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface IMagicSignInData {
|
||||
|
Loading…
Reference in New Issue
Block a user