diff --git a/.env.example b/.env.example index 42d98677b..29eae7cbe 100644 --- a/.env.example +++ b/.env.example @@ -65,4 +65,6 @@ NGINX_PORT=80 DEFAULT_EMAIL="captain@plane.so" DEFAULT_PASSWORD="password123" +# SignUps +ENABLE_SIGNUP="1" # Auto generated and Required that will be generated from setup.sh \ No newline at end of file diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index bf5180ff8..23277f294 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -5,6 +5,7 @@ from django.urls import path from plane.api.views import ( # Authentication + SignUpEndpoint, SignInEndpoint, SignOutEndpoint, MagicSignInEndpoint, @@ -154,6 +155,7 @@ urlpatterns = [ # Social Auth path("social-auth/", OauthEndpoint.as_view(), name="oauth"), # Auth + path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"), path("sign-in/", SignInEndpoint.as_view(), name="sign-in"), path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), # Magic Sign In/Up diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 4177b1371..45ea35f71 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -79,6 +79,7 @@ from .auth_extended import ( from .authentication import ( + SignUpEndpoint, SignInEndpoint, SignOutEndpoint, MagicSignInEndpoint, diff --git a/apiserver/plane/api/views/authentication.py b/apiserver/plane/api/views/authentication.py index a63f199ad..385ec7568 100644 --- a/apiserver/plane/api/views/authentication.py +++ b/apiserver/plane/api/views/authentication.py @@ -36,6 +36,99 @@ def get_tokens_for_user(user): ) +class SignUpEndpoint(BaseAPIView): + permission_classes = (AllowAny,) + + def post(self, request): + try: + if not settings.ENABLE_SIGNUP: + return Response( + { + "error": "New account creation is disabled. Please contact your site administrator" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + email = request.data.get("email", False) + password = request.data.get("password", False) + + ## Raise exception if any of the above are missing + if not email or not password: + return Response( + {"error": "Both email and password are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + email = email.strip().lower() + + try: + validate_email(email) + except ValidationError as e: + return Response( + {"error": "Please provide a valid email address."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Check if the user already exists + if User.objects.filter(email=email).exists(): + return Response( + {"error": "User already exist please sign in"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user = User.objects.create(email=email, username=uuid.uuid4().hex) + user.set_password(password) + + # settings last actives for the user + 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() + + serialized_user = UserSerializer(user).data + + access_token, refresh_token = get_tokens_for_user(user) + + data = { + "access_token": access_token, + "refresh_token": refresh_token, + "user": serialized_user, + } + + # Send Analytics + if settings.ANALYTICS_BASE_API: + _ = requests.post( + settings.ANALYTICS_BASE_API, + headers={ + "Content-Type": "application/json", + "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + }, + json={ + "event_id": uuid.uuid4().hex, + "event_data": { + "medium": "email", + }, + "user": {"email": email, "id": str(user.id)}, + "device_ctx": { + "ip": request.META.get("REMOTE_ADDR"), + "user_agent": request.META.get("HTTP_USER_AGENT"), + }, + "event_type": "SIGN_UP", + }, + ) + + return Response(data, status=status.HTTP_200_OK) + + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + class SignInEndpoint(BaseAPIView): permission_classes = (AllowAny,) @@ -63,108 +156,69 @@ class SignInEndpoint(BaseAPIView): user = User.objects.filter(email=email).first() - # Sign up Process if user is None: - user = User.objects.create(email=email, username=uuid.uuid4().hex) - user.set_password(password) + return Response( + { + "error": "Sorry, we could not find a user with the provided credentials. Please try again." + }, + status=status.HTTP_403_FORBIDDEN, + ) - # settings last actives for the user - 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() + # Sign up Process + 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, + ) + if not user.is_active: + return Response( + { + "error": "Your account has been deactivated. Please contact your site administrator." + }, + status=status.HTTP_403_FORBIDDEN, + ) - serialized_user = UserSerializer(user).data + serialized_user = UserSerializer(user).data - access_token, refresh_token = get_tokens_for_user(user) + # settings last active for the user + 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() - data = { - "access_token": access_token, - "refresh_token": refresh_token, - "user": serialized_user, - } - - # Send Analytics - if settings.ANALYTICS_BASE_API: - _ = requests.post( - settings.ANALYTICS_BASE_API, - headers={ - "Content-Type": "application/json", - "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + access_token, refresh_token = get_tokens_for_user(user) + # Send Analytics + if settings.ANALYTICS_BASE_API: + _ = requests.post( + settings.ANALYTICS_BASE_API, + headers={ + "Content-Type": "application/json", + "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + }, + json={ + "event_id": uuid.uuid4().hex, + "event_data": { + "medium": "email", }, - json={ - "event_id": uuid.uuid4().hex, - "event_data": { - "medium": "email", - }, - "user": {"email": email, "id": str(user.id)}, - "device_ctx": { - "ip": request.META.get("REMOTE_ADDR"), - "user_agent": request.META.get("HTTP_USER_AGENT"), - }, - "event_type": "SIGN_UP", + "user": {"email": email, "id": str(user.id)}, + "device_ctx": { + "ip": request.META.get("REMOTE_ADDR"), + "user_agent": request.META.get("HTTP_USER_AGENT"), }, - ) + "event_type": "SIGN_IN", + }, + ) + data = { + "access_token": access_token, + "refresh_token": refresh_token, + "user": serialized_user, + } - return Response(data, status=status.HTTP_200_OK) - # Sign in Process - else: - 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, - ) - if not user.is_active: - return Response( - { - "error": "Your account has been deactivated. Please contact your site administrator." - }, - status=status.HTTP_403_FORBIDDEN, - ) - - serialized_user = UserSerializer(user).data - - # settings last active for the user - 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) - # Send Analytics - if settings.ANALYTICS_BASE_API: - _ = requests.post( - settings.ANALYTICS_BASE_API, - headers={ - "Content-Type": "application/json", - "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, - }, - json={ - "event_id": uuid.uuid4().hex, - "event_data": { - "medium": "email", - }, - "user": {"email": email, "id": str(user.id)}, - "device_ctx": { - "ip": request.META.get("REMOTE_ADDR"), - "user_agent": request.META.get("HTTP_USER_AGENT"), - }, - "event_type": "SIGN_IN", - }, - ) - data = { - "access_token": access_token, - "refresh_token": refresh_token, - "user": serialized_user, - } - - return Response(data, status=status.HTTP_200_OK) + return Response(data, status=status.HTTP_200_OK) except Exception as e: capture_exception(e) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 2f3fcb558..c1cdcc4b4 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -2,7 +2,7 @@ import jwt from datetime import date, datetime from dateutil.relativedelta import relativedelta - +from uuid import uuid4 # Django imports from django.db import IntegrityError from django.db.models import Prefetch @@ -249,6 +249,17 @@ class InviteWorkspaceEndpoint(BaseAPIView): email__in=[email.get("email") for email in emails] ).select_related("workspace") + # create the user if signup is disabled + if settings.DOCKERIZED and not settings.ENABLE_SIGNUP: + _ = User.objects.bulk_create([ + User( + email=email.get("email"), + password=str(uuid4().hex), + is_password_autoset=True + ) + for email in emails + ], batch_size=100) + for invitation in workspace_invitations: workspace_invitation.delay( invitation.email, diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 20b257a27..3a3a3d9a3 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -91,3 +91,5 @@ CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL") CELERY_BROKER_URL = os.environ.get("REDIS_URL") GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) + +ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" \ No newline at end of file diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index 7e7f4186f..d5fcd3d04 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -258,3 +258,6 @@ else: CELERY_BROKER_URL = broker_url GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) + + +ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index c6ffcaf22..851ad77f2 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -211,3 +211,5 @@ CELERY_RESULT_BACKEND = broker_url CELERY_BROKER_URL = broker_url GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) + +ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" diff --git a/docker-compose.yml b/docker-compose.yml index 45a74afb6..ec998ab76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,6 +54,7 @@ services: DEFAULT_EMAIL: ${DEFAULT_EMAIL} DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} USE_MINIO: ${USE_MINIO} + ENABLE_SIGNUP: ${ENABLE_SIGNUP} depends_on: - plane-db - plane-redis @@ -91,6 +92,7 @@ services: DEFAULT_EMAIL: ${DEFAULT_EMAIL:-captain@plane.so} DEFAULT_PASSWORD: ${DEFAULT_PASSWORD:-password123} USE_MINIO: ${USE_MINIO} + ENABLE_SIGNUP: ${ENABLE_SIGNUP} depends_on: - plane-api - plane-db