dev: instance registration (#2912)

* dev: remove auto script for registration

* dev: make all of the instance admins as owners when adding a instance admin

* dev: remove sign out endpoint

* dev: update takeoff script to register the instance

* dev:  reapply instance model

* dev: check none for instance configuration encryptions

* dev: encrypting secrets configuration

* dev: user workflow for registration in instances

* dev: add email automation configuration

* dev: remove unused imports

* dev: reallign migrations

* dev: reconfigure license engine registrations

* dev: move email check to background worker

* dev: add sign up

* chore: signup error message

* dev: updated onboarding workflows and instance setting

* dev: updated template for magic login

* chore: page migration changed

* dev: updated migrations and authentication for license and update template for workspace invite

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Nikhil 2023-11-29 20:33:37 +05:30 committed by Aaryan Khandelwal
parent ea69d82ea5
commit c5cced1f19
40 changed files with 4414 additions and 1493 deletions

View File

@ -3,16 +3,26 @@ set -e
python manage.py wait_for_db python manage.py wait_for_db
python manage.py migrate python manage.py migrate
# Set default value for ENABLE_REGISTRATION # Create the default bucket
ENABLE_REGISTRATION=${ENABLE_REGISTRATION:-1} #!/bin/bash
# Check if ENABLE_REGISTRATION is not set to '0' # Collect system information
if [ "$ENABLE_REGISTRATION" != "0" ]; then HOSTNAME=$(hostname)
# Register instance MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
python manage.py register_instance CPU_INFO=$(cat /proc/cpuinfo)
# Load the configuration variable MEMORY_INFO=$(free -h)
python manage.py configure_instance DISK_INFO=$(df -h)
fi
# Concatenate information and compute SHA-256 hash
SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
# Export the variables
export MACHINE_SIGNATURE=$SIGNATURE
# Register instance
python manage.py register_instance $MACHINE_SIGNATURE
# Load the configuration variable
python manage.py configure_instance
# Create the default bucket # Create the default bucket
python manage.py create_bucket python manage.py create_bucket

View File

@ -99,7 +99,6 @@ class WorkspaceViewerPermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, member=request.user,
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
role__gte=10,
is_active=True, is_active=True,
).exists() ).exists()

View File

@ -26,6 +26,8 @@ class UserSerializer(BaseSerializer):
"token_updated_at", "token_updated_at",
"is_onboarded", "is_onboarded",
"is_bot", "is_bot",
"is_password_autoset",
"is_email_verified",
] ]
extra_kwargs = {"password": {"write_only": True}} extra_kwargs = {"password": {"write_only": True}}
@ -60,6 +62,8 @@ class UserMeSerializer(BaseSerializer):
"theme", "theme",
"last_workspace_id", "last_workspace_id",
"use_case", "use_case",
"is_password_autoset",
"is_email_verified",
] ]
read_only_fields = fields read_only_fields = fields
@ -189,4 +193,3 @@ class ResetPasswordSerializer(serializers.Serializer):
Serializer for password change endpoint. Serializer for password change endpoint.
""" """
new_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True)
confirm_password = serializers.CharField(required=True)

View File

@ -34,6 +34,7 @@ class WorkSpaceSerializer(BaseSerializer):
"profile", "profile",
"spaces", "spaces",
"workspace-invitations", "workspace-invitations",
"password",
]: ]:
raise serializers.ValidationError({"slug": "Slug is not valid"}) raise serializers.ValidationError({"slug": "Slug is not valid"})

View File

@ -5,18 +5,15 @@ from rest_framework_simplejwt.views import TokenRefreshView
from plane.app.views import ( from plane.app.views import (
# Authentication # Authentication
SignUpEndpoint,
SignInEndpoint, SignInEndpoint,
SignOutEndpoint, SignOutEndpoint,
MagicSignInEndpoint, MagicSignInEndpoint,
MagicSignInGenerateEndpoint,
OauthEndpoint, OauthEndpoint,
EmailCheckEndpoint,
## End Authentication ## End Authentication
# Auth Extended # Auth Extended
ForgotPasswordEndpoint, ForgotPasswordEndpoint,
VerifyEmailEndpoint,
ResetPasswordEndpoint, ResetPasswordEndpoint,
RequestEmailVerificationEndpoint,
ChangePasswordEndpoint, ChangePasswordEndpoint,
## End Auth Extender ## End Auth Extender
# API Tokens # API Tokens
@ -27,24 +24,14 @@ from plane.app.views import (
urlpatterns = [ urlpatterns = [
# Social Auth # Social Auth
path("email-check/", EmailCheckEndpoint.as_view(), name="email"),
path("social-auth/", OauthEndpoint.as_view(), name="oauth"), path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
# Auth # Auth
path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"),
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"), path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
# Magic Sign In/Up # magic sign in
path(
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
),
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"), path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
# Email verification
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
path(
"request-email-verify/",
RequestEmailVerificationEndpoint.as_view(),
name="request-reset-email",
),
# Password Manipulation # Password Manipulation
path( path(
"users/me/change-password/", "users/me/change-password/",

View File

@ -7,6 +7,7 @@ from plane.app.views import (
UpdateUserTourCompletedEndpoint, UpdateUserTourCompletedEndpoint,
UserActivityEndpoint, UserActivityEndpoint,
ChangePasswordEndpoint, ChangePasswordEndpoint,
SetUserPasswordEndpoint,
## End User ## End User
## Workspaces ## Workspaces
UserWorkSpacesEndpoint, UserWorkSpacesEndpoint,
@ -89,5 +90,10 @@ urlpatterns = [
UserWorkspaceDashboardEndpoint.as_view(), UserWorkspaceDashboardEndpoint.as_view(),
name="user-workspace-dashboard", name="user-workspace-dashboard",
), ),
path(
"users/me/set-password/",
SetUserPasswordEndpoint.as_view(),
name="set-password",
),
## End User Graph ## End User Graph
] ]

View File

@ -82,20 +82,18 @@ from .issue import (
) )
from .auth_extended import ( from .auth_extended import (
VerifyEmailEndpoint,
RequestEmailVerificationEndpoint,
ForgotPasswordEndpoint, ForgotPasswordEndpoint,
ResetPasswordEndpoint, ResetPasswordEndpoint,
ChangePasswordEndpoint, ChangePasswordEndpoint,
SetUserPasswordEndpoint,
EmailCheckEndpoint,
) )
from .authentication import ( from .authentication import (
SignUpEndpoint,
SignInEndpoint, SignInEndpoint,
SignOutEndpoint, SignOutEndpoint,
MagicSignInEndpoint, MagicSignInEndpoint,
MagicSignInGenerateEndpoint,
) )
from .module import ( from .module import (
@ -164,4 +162,8 @@ from .exporter import ExportIssuesEndpoint
from .config import ConfigurationEndpoint from .config import ConfigurationEndpoint
from .webhook import WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint from .webhook import (
WebhookEndpoint,
WebhookLogsEndpoint,
WebhookSecretRegenerateEndpoint,
)

View File

@ -1,5 +1,9 @@
## Python imports ## Python imports
import jwt import uuid
import os
import json
import random
import string
## Django imports ## Django imports
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
@ -8,65 +12,95 @@ from django.utils.encoding import (
smart_bytes, smart_bytes,
DjangoUnicodeDecodeError, DjangoUnicodeDecodeError,
) )
from django.contrib.auth.hashers import make_password
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from django.conf import settings from django.conf import settings
## Third Party Imports ## Third Party Imports
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import permissions from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from sentry_sdk import capture_exception
## Module imports ## Module imports
from . import BaseAPIView from . import BaseAPIView
from plane.app.serializers import ( from plane.app.serializers import (
ChangePasswordSerializer, ChangePasswordSerializer,
ResetPasswordSerializer, ResetPasswordSerializer,
UserSerializer,
) )
from plane.db.models import User from plane.db.models import User, WorkspaceMemberInvite
from plane.bgtasks.email_verification_task import email_verification from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.forgot_password_task import forgot_password 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 (
str(refresh.access_token),
str(refresh),
)
class RequestEmailVerificationEndpoint(BaseAPIView): def generate_magic_token(email):
def get(self, request): key = "magic_" + str(email)
token = RefreshToken.for_user(request.user).access_token
current_site = request.META.get('HTTP_ORIGIN') ## Generate a random token
email_verification.delay( token = (
request.user.first_name, request.user.email, token, current_site "".join(random.choices(string.ascii_lowercase, k=4))
) + "-"
return Response( + "".join(random.choices(string.ascii_lowercase, k=4))
{"message": "Email sent successfully"}, status=status.HTTP_200_OK + "-"
) + "".join(random.choices(string.ascii_lowercase, k=4))
)
# Initialize the redis instance
ri = redis_instance()
# 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 key, token, False
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)
return key, token, True
class VerifyEmailEndpoint(BaseAPIView): def generate_password_token(user):
def get(self, request): uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = request.GET.get("token") token = PasswordResetTokenGenerator().make_token(user)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
user = User.objects.get(id=payload["user_id"])
if not user.is_email_verified: return uidb64, token
user.is_email_verified = True
user.save()
return Response(
{"email": "Successfully activated"}, status=status.HTTP_200_OK
)
except jwt.ExpiredSignatureError as _indentifier:
return Response(
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
)
except jwt.exceptions.DecodeError as _indentifier:
return Response(
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
)
class ForgotPasswordEndpoint(BaseAPIView): class ForgotPasswordEndpoint(BaseAPIView):
permission_classes = [permissions.AllowAny] permission_classes = [
AllowAny,
]
def post(self, request): def post(self, request):
email = request.data.get("email") email = request.data.get("email")
@ -76,7 +110,7 @@ class ForgotPasswordEndpoint(BaseAPIView):
uidb64 = urlsafe_base64_encode(smart_bytes(user.id)) uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = PasswordResetTokenGenerator().make_token(user) token = PasswordResetTokenGenerator().make_token(user)
current_site = request.META.get('HTTP_ORIGIN') current_site = request.META.get("HTTP_ORIGIN")
forgot_password.delay( forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site user.first_name, user.email, uidb64, token, current_site
@ -92,7 +126,7 @@ class ForgotPasswordEndpoint(BaseAPIView):
class ResetPasswordEndpoint(BaseAPIView): class ResetPasswordEndpoint(BaseAPIView):
permission_classes = [permissions.AllowAny] permission_classes = [AllowAny,]
def post(self, request, uidb64, token): def post(self, request, uidb64, token):
try: try:
@ -100,22 +134,26 @@ class ResetPasswordEndpoint(BaseAPIView):
user = User.objects.get(id=id) user = User.objects.get(id=id)
if not PasswordResetTokenGenerator().check_token(user, token): if not PasswordResetTokenGenerator().check_token(user, token):
return Response( return Response(
{"error": "token is not valid, please check the new one"}, {"error": "Token is invalid"},
status=status.HTTP_401_UNAUTHORIZED, status=status.HTTP_401_UNAUTHORIZED,
) )
serializer = ResetPasswordSerializer(data=request.data)
serializer = ResetPasswordSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
# set_password also hashes the password that the user will get # set_password also hashes the password that the user will get
user.set_password(serializer.data.get("new_password")) user.set_password(serializer.data.get("new_password"))
user.is_password_autoset = False
user.save() user.save()
response = {
"status": "success", # Generate access token for the user
"code": status.HTTP_200_OK, access_token, refresh_token = get_tokens_for_user(user)
"message": "Password updated successfully",
data = {
"access_token": access_token,
"refresh_token": refresh_token,
} }
return Response(response) return Response(data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except DjangoUnicodeDecodeError as indentifier: except DjangoUnicodeDecodeError as indentifier:
@ -138,6 +176,208 @@ class ChangePasswordEndpoint(BaseAPIView):
) )
# set_password also hashes the password that the user will get # set_password also hashes the password that the user will get
user.set_password(serializer.data.get("new_password")) user.set_password(serializer.data.get("new_password"))
user.is_password_autoset = False
user.save() user.save()
return Response({"message": "Password updated successfully"}, status=status.HTTP_200_OK) return Response(
{"message": "Password updated successfully"}, status=status.HTTP_200_OK
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class SetUserPasswordEndpoint(BaseAPIView):
def post(self, request):
user = User.objects.get(pk=request.user.id)
password = request.data.get("password", False)
# If the user password is not autoset then return error
if not user.is_password_autoset:
return Response(
{
"error": "Your password is already set please change your password from profile"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Check password validation
if not password and len(str(password)) < 8:
return Response(
{"error": "Password is not valid"}, status=status.HTTP_400_BAD_REQUEST
)
# 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)
class EmailCheckEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def post(self, request):
# get the email
# Check the instance registration
instance = Instance.objects.first()
if instance is None:
return Response(
{"error": "Instance is not configured"},
status=status.HTTP_400_BAD_REQUEST,
)
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)
# validate the email
try:
validate_email(email)
except ValidationError:
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()
current_site = request.META.get("HTTP_ORIGIN")
# If new user
if user is None:
# 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(uuid.uuid4().hex),
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(
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
)
# 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_400_BAD_REQUEST)
# Existing user
else:
if type == "magic_code":
## Generate a random token
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,
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=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)
# 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)
else:
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=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)

View File

@ -4,8 +4,6 @@ import uuid
import random import random
import string import string
import json import json
import requests
from requests.exceptions import RequestException
# Django imports # Django imports
from django.utils import timezone from django.utils import timezone
@ -20,7 +18,7 @@ from rest_framework.permissions import AllowAny
from rest_framework import status from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from sentry_sdk import capture_exception, capture_message from sentry_sdk import capture_message
# Module imports # Module imports
from . import BaseAPIView from . import BaseAPIView
@ -32,10 +30,12 @@ from plane.db.models import (
ProjectMember, ProjectMember,
) )
from plane.settings.redis import redis_instance from plane.settings.redis import redis_instance
from plane.bgtasks.magic_link_code_task import magic_link from plane.license.models import InstanceConfiguration, Instance
from plane.license.models import InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.event_tracking_task import auth_events from plane.bgtasks.event_tracking_task import auth_events
from plane.bgtasks.magic_link_code_task import magic_link
from plane.bgtasks.user_count_task import update_user_instance_user_count
def get_tokens_for_user(user): def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user) refresh = RefreshToken.for_user(user)
@ -50,22 +50,6 @@ class SignUpEndpoint(BaseAPIView):
def post(self, request): def post(self, request):
instance_configuration = InstanceConfiguration.objects.values("key", "value") instance_configuration = InstanceConfiguration.objects.values("key", "value")
if (
not get_configuration_value(
instance_configuration,
"ENABLE_SIGNUP",
os.environ.get("ENABLE_SIGNUP", "0"),
)
and not WorkspaceMemberInvite.objects.filter(
email=request.user.email
).exists()
):
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) email = request.data.get("email", False)
password = request.data.get("password", False) password = request.data.get("password", False)
@ -87,6 +71,24 @@ class SignUpEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
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,
)
# Check if the user already exists # Check if the user already exists
if User.objects.filter(email=email).exists(): if User.objects.filter(email=email).exists():
return Response( return Response(
@ -105,81 +107,16 @@ class SignUpEndpoint(BaseAPIView):
user.token_updated_at = timezone.now() user.token_updated_at = timezone.now()
user.save() user.save()
# Check if user has any accepted invites for workspace and add them to workspace
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
email=user.email, accepted=True
)
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=workspace_member_invite.workspace_id,
member=user,
role=workspace_member_invite.role,
)
for workspace_member_invite in workspace_member_invites
],
ignore_conflicts=True,
)
# Check if user has any project invites
project_member_invites = ProjectMemberInvite.objects.filter(
email=user.email, accepted=True
)
# Add user to workspace
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=project_member_invite.workspace_id,
role=project_member_invite.role
if project_member_invite.role in [5, 10, 15]
else 15,
member=user,
created_by_id=project_member_invite.created_by_id,
)
for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Now add the users to project
ProjectMember.objects.bulk_create(
[
ProjectMember(
workspace_id=project_member_invite.workspace_id,
role=project_member_invite.role
if project_member_invite.role in [5, 10, 15]
else 15,
member=user,
created_by_id=project_member_invite.created_by_id,
)
for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Delete all the invites
workspace_member_invites.delete()
project_member_invites.delete()
# 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
)
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
data = { data = {
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
} }
# Update instance user count
update_user_instance_user_count.delay()
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
@ -207,8 +144,18 @@ class SignInEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# Check if the instance setup is done or not
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,
)
# Get the user
user = User.objects.filter(email=email).first() user = User.objects.filter(email=email).first()
# User is not present in db
if user is None: if user is None:
return Response( return Response(
{ {
@ -217,7 +164,7 @@ class SignInEndpoint(BaseAPIView):
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
# Sign up Process # Check user password
if not user.check_password(password): if not user.check_password(password):
return Response( return Response(
{ {
@ -292,7 +239,7 @@ class SignInEndpoint(BaseAPIView):
# Delete all the invites # Delete all the invites
workspace_member_invites.delete() workspace_member_invites.delete()
project_member_invites.delete() project_member_invites.delete()
# Send event # Send event
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
auth_events.delay( auth_events.delay(
user=user.id, user=user.id,
@ -301,7 +248,7 @@ class SignInEndpoint(BaseAPIView):
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="SIGN_IN",
medium="EMAIL", medium="EMAIL",
first_time=False first_time=False,
) )
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
@ -335,101 +282,19 @@ class SignOutEndpoint(BaseAPIView):
return Response({"message": "success"}, status=status.HTTP_200_OK) return Response({"message": "success"}, status=status.HTTP_200_OK)
class MagicSignInGenerateEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def post(self, request):
email = request.data.get("email", False)
instance_configuration = InstanceConfiguration.objects.values("key", "value")
if (
not get_configuration_value(
instance_configuration,
"ENABLE_MAGIC_LINK_LOGIN",
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
)
and not (
get_configuration_value(
instance_configuration,
"ENABLE_SIGNUP",
os.environ.get("ENABLE_SIGNUP", "0"),
)
)
and not WorkspaceMemberInvite.objects.filter(
email=request.user.email
).exists()
):
return Response(
{
"error": "New account creation is disabled. Please contact your site administrator"
},
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)
## 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)
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 MagicSignInEndpoint(BaseAPIView): class MagicSignInEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
AllowAny, AllowAny,
] ]
def post(self, request): def post(self, request):
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,
)
user_token = request.data.get("token", "").strip() user_token = request.data.get("token", "").strip()
key = request.data.get("key", False).strip().lower() key = request.data.get("key", False).strip().lower()
@ -448,48 +313,28 @@ class MagicSignInEndpoint(BaseAPIView):
email = data["email"] email = data["email"]
if str(token) == str(user_token): if str(token) == str(user_token):
if User.objects.filter(email=email).exists(): user = User.objects.get(email=email)
user = User.objects.get(email=email) if not user.is_active:
if not user.is_active: return Response(
return Response( {
{ "error": "Your account has been deactivated. Please contact your site administrator."
"error": "Your account has been deactivated. Please contact your site administrator." },
}, status=status.HTTP_403_FORBIDDEN,
status=status.HTTP_403_FORBIDDEN, )
) # Send event
# Send event if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: auth_events.delay(
auth_events.delay( user=user.id,
user=user.id,
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=False
)
else:
user = User.objects.create(
email=email, email=email,
username=uuid.uuid4().hex, user_agent=request.META.get("HTTP_USER_AGENT"),
password=make_password(uuid.uuid4().hex), ip=request.META.get("REMOTE_ADDR"),
is_password_autoset=True, event_name="SIGN_IN",
medium="MAGIC_LINK",
first_time=False,
) )
# 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
)
user.is_active = True user.is_active = True
user.is_email_verified = True
user.last_active = timezone.now() user.last_active = timezone.now()
user.last_login_time = timezone.now() user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR") user.last_login_ip = request.META.get("REMOTE_ADDR")

View File

@ -2,7 +2,6 @@
import uuid import uuid
import requests import requests
import os import os
from requests.exceptions import RequestException
# Django imports # Django imports
from django.utils import timezone from django.utils import timezone
@ -31,8 +30,9 @@ from plane.db.models import (
) )
from plane.bgtasks.event_tracking_task import auth_events from plane.bgtasks.event_tracking_task import auth_events
from .base import BaseAPIView from .base import BaseAPIView
from plane.license.models import InstanceConfiguration from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.user_count_task import update_user_instance_user_count
def get_tokens_for_user(user): def get_tokens_for_user(user):
@ -136,6 +136,14 @@ class OauthEndpoint(BaseAPIView):
def post(self, request): def post(self, request):
try: try:
# Check if instance is registered or not
instance = Instance.objects.first()
if instance is None and not instance.is_setup_done:
return Response(
{"error": "Instance is not configured"},
status=status.HTTP_400_BAD_REQUEST,
)
medium = request.data.get("medium", False) medium = request.data.get("medium", False)
id_token = request.data.get("credential", False) id_token = request.data.get("credential", False)
client_id = request.data.get("clientId", False) client_id = request.data.get("clientId", False)
@ -143,34 +151,17 @@ class OauthEndpoint(BaseAPIView):
instance_configuration = InstanceConfiguration.objects.values( instance_configuration = InstanceConfiguration.objects.values(
"key", "value" "key", "value"
) )
if ( if not get_configuration_value(
( instance_configuration,
not get_configuration_value( "GOOGLE_CLIENT_ID",
instance_configuration, os.environ.get("GOOGLE_CLIENT_ID"),
"GOOGLE_CLIENT_ID", ) or not get_configuration_value(
os.environ.get("GOOGLE_CLIENT_ID"), instance_configuration,
) "GITHUB_CLIENT_ID",
or not get_configuration_value( os.environ.get("GITHUB_CLIENT_ID"),
instance_configuration,
"GITHUB_CLIENT_ID",
os.environ.get("GITHUB_CLIENT_ID"),
)
)
and not (
get_configuration_value(
instance_configuration,
"ENABLE_SIGNUP",
os.environ.get("ENABLE_SIGNUP", "0"),
)
)
and not WorkspaceMemberInvite.objects.filter(
email=request.user.email
).exists()
): ):
return Response( return Response(
{ {"error": "Github or Google login is not configured"},
"error": "New account creation is disabled. Please contact your site administrator"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -286,8 +277,8 @@ class OauthEndpoint(BaseAPIView):
"last_login_at": timezone.now(), "last_login_at": timezone.now(),
}, },
) )
# Send event # Send event
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
auth_events.delay( auth_events.delay(
user=user.id, user=user.id,
@ -295,8 +286,8 @@ class OauthEndpoint(BaseAPIView):
user_agent=request.META.get("HTTP_USER_AGENT"), user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="SIGN_IN",
medium=medium.upper(), medium=medium.upper(),
first_time=False first_time=False,
) )
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
@ -309,6 +300,16 @@ class OauthEndpoint(BaseAPIView):
except User.DoesNotExist: except User.DoesNotExist:
## Signup Case ## Signup Case
instance_configuration = InstanceConfiguration.objects.values(
"key", "value"
)
# Check if instance is registered or not
instance = Instance.objects.first()
if instance is None and not instance.is_setup_done:
return Response(
{"error": "Instance is not configured"},
status=status.HTTP_400_BAD_REQUEST,
)
if ( if (
get_configuration_value( get_configuration_value(
@ -316,8 +317,9 @@ class OauthEndpoint(BaseAPIView):
"ENABLE_SIGNUP", "ENABLE_SIGNUP",
os.environ.get("ENABLE_SIGNUP", "0"), os.environ.get("ENABLE_SIGNUP", "0"),
) )
== "0"
and not WorkspaceMemberInvite.objects.filter( and not WorkspaceMemberInvite.objects.filter(
email=request.user.email email=email,
).exists() ).exists()
): ):
return Response( return Response(
@ -341,7 +343,7 @@ class OauthEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
user = User( user = User.objects.create(
username=username, username=username,
email=email, email=email,
mobile_number=mobile_number, mobile_number=mobile_number,
@ -352,7 +354,6 @@ class OauthEndpoint(BaseAPIView):
) )
user.set_password(uuid.uuid4().hex) user.set_password(uuid.uuid4().hex)
user.is_password_autoset = True
user.last_active = timezone.now() user.last_active = timezone.now()
user.last_login_time = timezone.now() user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR") user.last_login_ip = request.META.get("REMOTE_ADDR")
@ -418,7 +419,7 @@ class OauthEndpoint(BaseAPIView):
workspace_member_invites.delete() workspace_member_invites.delete()
project_member_invites.delete() project_member_invites.delete()
# Send event # Send event
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
auth_events.delay( auth_events.delay(
user=user.id, user=user.id,
@ -427,7 +428,7 @@ class OauthEndpoint(BaseAPIView):
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="SIGN_IN",
medium=medium.upper(), medium=medium.upper(),
first_time=True first_time=True,
) )
SocialLoginConnection.objects.update_or_create( SocialLoginConnection.objects.update_or_create(
@ -445,4 +446,7 @@ class OauthEndpoint(BaseAPIView):
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
} }
# Update the user count
update_user_instance_user_count.delay()
return Response(data, status=status.HTTP_201_CREATED) return Response(data, status=status.HTTP_201_CREATED)

View File

@ -17,7 +17,7 @@ from plane.license.models import Instance, InstanceAdmin
from plane.utils.paginator import BasePaginator from plane.utils.paginator import BasePaginator
from django.db.models import Q, F, Count, Case, When, Value, IntegerField from django.db.models import Q, F, Count, Case, When, IntegerField
class UserEndpoint(BaseViewSet): class UserEndpoint(BaseViewSet):
@ -52,7 +52,6 @@ class UserEndpoint(BaseViewSet):
projects_to_deactivate = [] projects_to_deactivate = []
workspaces_to_deactivate = [] workspaces_to_deactivate = []
projects = ProjectMember.objects.filter( projects = ProjectMember.objects.filter(
member=request.user, is_active=True member=request.user, is_active=True
).annotate( ).annotate(
@ -155,3 +154,4 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
issue_activities, many=True issue_activities, many=True
).data, ).data,
) )

View File

@ -1,90 +0,0 @@
# Python imports
import os
# Django imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.license.models import InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value
@shared_task
def email_verification(first_name, email, token, current_site):
try:
realtivelink = "/request-email-verification/" + "?token=" + str(token)
abs_url = current_site + realtivelink
subject = "Verify your Email!"
context = {
"first_name": first_name,
"verification_url": abs_url,
}
html_content = render_to_string("emails/auth/email_verification.html", context)
text_content = strip_tags(html_content)
# Configure email connection from the database
instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_"
).values("key", "value")
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"),
)
),
)
# Initiate email alternatives
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>"),
),
to=[email],
connection=connection,
)
msg.attach_alternative(html_content, "text/html")
msg.send()
return
except Exception as e:
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
capture_exception(e)
return

View File

@ -1,5 +1,7 @@
# Python import # Python import
import os import os
import requests
import json
# Django imports # Django imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
@ -12,17 +14,61 @@ from celery import shared_task
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# Module imports # Module imports
from plane.license.models import InstanceConfiguration from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_email_configuration
@shared_task @shared_task
def forgot_password(first_name, email, uidb64, token, current_site): def forgot_password(first_name, email, uidb64, token, current_site):
try: try:
realtivelink = f"/accounts/reset-password/?uidb64={uidb64}&token={token}" relative_link = (
abs_url = current_site + realtivelink f"/accounts/password/?uidb64={uidb64}&token={token}&email={email}"
)
abs_url = current_site + relative_link
subject = "Reset Your Password - Plane" 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)
# Send the email if the users don't have smtp configured
if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD:
# Check the instance registration
instance = Instance.objects.first()
# send the emails through control center
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
# 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"{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 = { context = {
"first_name": first_name, "first_name": first_name,
@ -33,43 +79,21 @@ def forgot_password(first_name, email, uidb64, token, current_site):
text_content = strip_tags(html_content) text_content = strip_tags(html_content)
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value") instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_"
).values("key", "value")
connection = get_connection( connection = get_connection(
host=get_configuration_value( host=EMAIL_HOST,
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST") port=int(EMAIL_PORT),
), username=EMAIL_HOST_USER,
port=int( password=EMAIL_HOST_PASSWORD,
get_configuration_value( use_tls=bool(EMAIL_USE_TLS),
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
)
),
username=get_configuration_value(
instance_configuration,
"EMAIL_HOST_USER",
os.environ.get("EMAIL_HOST_USER"),
),
password=get_configuration_value(
instance_configuration,
"EMAIL_HOST_PASSWORD",
os.environ.get("EMAIL_HOST_PASSWORD"),
),
use_tls=bool(
get_configuration_value(
instance_configuration,
"EMAIL_USE_TLS",
os.environ.get("EMAIL_USE_TLS", "1"),
)
),
) )
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
subject=subject, subject=subject,
body=text_content, body=text_content,
from_email=get_configuration_value( from_email=EMAIL_FROM,
instance_configuration,
"EMAIL_FROM",
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
),
to=[email], to=[email],
connection=connection, connection=connection,
) )

View File

@ -26,6 +26,7 @@ from plane.db.models import (
IssueProperty, IssueProperty,
) )
from plane.bgtasks.user_welcome_task import send_welcome_slack from plane.bgtasks.user_welcome_task import send_welcome_slack
from plane.bgtasks.user_count_task import update_user_instance_user_count
@shared_task @shared_task
@ -120,6 +121,9 @@ def service_importer(service, importer_id):
batch_size=100, batch_size=100,
ignore_conflicts=True, ignore_conflicts=True,
) )
# Update instance user count
update_user_instance_user_count.delay()
# Check if sync config is on for github importers # Check if sync config is on for github importers
if service == "github" and importer.config.get("sync", False): if service == "github" and importer.config.get("sync", False):

View File

@ -1,5 +1,7 @@
# Python imports # Python imports
import os import os
import requests
import json
# Django imports # Django imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
@ -12,63 +14,78 @@ from celery import shared_task
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# Module imports # Module imports
from plane.license.models import InstanceConfiguration from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_email_configuration
@shared_task @shared_task
def magic_link(email, key, token, current_site): def magic_link(email, key, token, current_site):
try: try:
realtivelink = f"/magic-sign-in/?password={token}&key={key}" if current_site:
abs_url = current_site + realtivelink realtivelink = f"/magic-sign-in/?password={token}&key={key}"
abs_url = current_site + realtivelink
subject = "Login for Plane" else:
abs_url = ""
context = {"magic_url": abs_url, "code": token}
html_content = render_to_string("emails/auth/magic_signin.html", context)
text_content = strip_tags(html_content)
instance_configuration = InstanceConfiguration.objects.filter( instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_" key__startswith="EMAIL_"
).values("key", "value") ).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)
# Send the email if the users don't have smtp configured
if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD:
# Check the instance registration
instance = Instance.objects.first()
# send the emails through control center
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
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"{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}
html_content = render_to_string("emails/auth/magic_signin.html", context)
text_content = strip_tags(html_content)
connection = get_connection( connection = get_connection(
host=get_configuration_value( host=EMAIL_HOST,
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST") port=int(EMAIL_PORT),
), username=EMAIL_HOST_USER,
port=int( password=EMAIL_HOST_PASSWORD,
get_configuration_value( use_tls=bool(EMAIL_USE_TLS),
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
)
),
username=get_configuration_value(
instance_configuration,
"EMAIL_HOST_USER",
os.environ.get("EMAIL_HOST_USER"),
),
password=get_configuration_value(
instance_configuration,
"EMAIL_HOST_PASSWORD",
os.environ.get("EMAIL_HOST_PASSWORD"),
),
use_tls=bool(
get_configuration_value(
instance_configuration,
"EMAIL_USE_TLS",
os.environ.get("EMAIL_USE_TLS", "1"),
)
),
) )
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
subject=subject, subject=subject,
body=text_content, body=text_content,
from_email=get_configuration_value( from_email=EMAIL_FROM,
instance_configuration,
"EMAIL_FROM",
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
),
to=[email], to=[email],
connection=connection, connection=connection,
) )

View File

@ -0,0 +1,49 @@
# 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,
}
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
if not license_engine_base_url:
raise Exception("License Engine base url is required")
# Update the license engine
_ = requests.post(
f"{license_engine_base_url}/api/instances/",
headers=headers,
data=json.dumps(payload),
)
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)

View File

@ -1,5 +1,7 @@
# Python imports # Python imports
import os import os
import requests
import json
# Django imports # Django imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
@ -15,8 +17,8 @@ from slack_sdk.errors import SlackApiError
# Module imports # Module imports
from plane.db.models import Workspace, WorkspaceMemberInvite, User from plane.db.models import Workspace, WorkspaceMemberInvite, User
from plane.license.models import InstanceConfiguration from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_email_configuration
@shared_task @shared_task
@ -35,14 +37,56 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
# The complete url including the domain # The complete url including the domain
abs_url = current_site + relative_link abs_url = current_site + relative_link
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)
# Send the email if the users don't have smtp configured
if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD:
# Check the instance registration
instance = Instance.objects.first()
# send the emails through control center
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
if not license_engine_base_url:
raise Exception("License engine base url is required")
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"{license_engine_base_url}/api/instances/users/workspace-invitation/",
headers=headers,
data=json.dumps(payload),
)
return
# Subject of the email # Subject of the email
subject = f"{user.first_name or user.display_name or user.email} invited you to join {workspace.name} on Plane" subject = f"{user.first_name or user.display_name or user.email} has invited you to join them in {workspace.name} on Plane"
context = { context = {
"email": email, "email": email,
"first_name": invitor, "first_name": user.first_name or user.display_name or user.email,
"workspace_name": workspace.name, "workspace_name": workspace.name,
"invitation_url": abs_url,
} }
html_content = render_to_string( html_content = render_to_string(
@ -58,41 +102,17 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
key__startswith="EMAIL_" key__startswith="EMAIL_"
).values("key", "value") ).values("key", "value")
connection = get_connection( connection = get_connection(
host=get_configuration_value( host=EMAIL_HOST,
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST") port=int(EMAIL_PORT),
), username=EMAIL_HOST_USER,
port=int( password=EMAIL_HOST_PASSWORD,
get_configuration_value( use_tls=bool(EMAIL_USE_TLS),
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
)
),
username=get_configuration_value(
instance_configuration,
"EMAIL_HOST_USER",
os.environ.get("EMAIL_HOST_USER"),
),
password=get_configuration_value(
instance_configuration,
"EMAIL_HOST_PASSWORD",
os.environ.get("EMAIL_HOST_PASSWORD"),
),
use_tls=bool(
get_configuration_value(
instance_configuration,
"EMAIL_USE_TLS",
os.environ.get("EMAIL_USE_TLS", "1"),
)
),
) )
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
subject=subject, subject=subject,
body=text_content, body=text_content,
from_email=get_configuration_value( from_email=EMAIL_FROM,
instance_configuration,
"EMAIL_FROM",
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
),
to=[email], to=[email],
connection=connection, connection=connection,
) )

View File

@ -21,7 +21,7 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('transaction', models.UUIDField(default=uuid.uuid4)), ('transaction', models.UUIDField(default=uuid.uuid4)),
('entity_identifier', models.UUIDField(null=True)), ('entity_identifier', models.UUIDField(null=True)),
('entity_name', models.CharField(choices=[('to_do', 'To Do'), ('issue', 'issue'), ('image', 'Image'), ('video', 'Video'), ('file', 'File'), ('link', 'Link'), ('cycle', 'Cycle'), ('module', 'Module'), ('back_link', 'Back Link'), ('forward_link', 'Forward Link'), ('mention', 'Mention')], max_length=30, verbose_name='Transaction Type')), ('entity_name', models.CharField(choices=[('to_do', 'To Do'), ('issue', 'issue'), ('image', 'Image'), ('video', 'Video'), ('file', 'File'), ('link', 'Link'), ('cycle', 'Cycle'), ('module', 'Module'), ('back_link', 'Back Link'), ('forward_link', 'Forward Link'), ('page_mention', 'Page Mention'), ('user_mention', 'User Mention')], max_length=30, verbose_name='Transaction Type')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_log', to='db.page')), ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_log', to='db.page')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),

View File

@ -1,6 +1,11 @@
# Generated by Django 4.2.5 on 2023-11-17 08:48 # Generated by Django 4.2.5 on 2023-11-17 08:48
from django.db import migrations, models from django.db import migrations, models
import plane.db.models.workspace
def user_password_autoset(apps, schema_editor):
User = apps.get_model("db", "User")
User.objects.update(is_password_autoset=True)
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -20,4 +25,15 @@ class Migration(migrations.Migration):
name='organization_size', name='organization_size',
field=models.CharField(blank=True, max_length=20, null=True), field=models.CharField(blank=True, max_length=20, null=True),
), ),
migrations.AddField(
model_name='fileasset',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='workspace',
name='slug',
field=models.SlugField(max_length=48, unique=True, validators=[plane.db.models.workspace.slug_validator]),
),
migrations.RunPython(user_password_autoset),
] ]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.7 on 2023-11-20 08:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0050_user_use_case_alter_workspace_organization_size'),
]
operations = [
migrations.AddField(
model_name='fileasset',
name='is_deleted',
field=models.BooleanField(default=False),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 4.2.5 on 2023-11-23 14:57
from django.db import migrations, models
import plane.db.models.workspace
class Migration(migrations.Migration):
dependencies = [
('db', '0051_fileasset_is_deleted'),
]
operations = [
migrations.AlterField(
model_name='workspace',
name='slug',
field=models.SlugField(max_length=48, unique=True, validators=[plane.db.models.workspace.slug_validator]),
),
]

View File

@ -7,7 +7,6 @@ from django.db import models
from django.conf import settings from django.conf import settings
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -141,8 +140,10 @@ class Issue(ProjectBaseModel):
)["largest"] )["largest"]
# aggregate can return None! Check it first. # aggregate can return None! Check it first.
# If it isn't none, just use the last ID specified (which should be the greatest) and add one to it # If it isn't none, just use the last ID specified (which should be the greatest) and add one to it
if last_id is not None: if last_id:
self.sequence_id = last_id + 1 self.sequence_id = last_id + 1
else:
self.sequence_id = 1
largest_sort_order = Issue.objects.filter( largest_sort_order = Issue.objects.filter(
project=self.project, state=self.state project=self.project, state=self.state

View File

@ -57,7 +57,8 @@ class PageLog(ProjectBaseModel):
("module", "Module"), ("module", "Module"),
("back_link", "Back Link"), ("back_link", "Back Link"),
("forward_link", "Forward Link"), ("forward_link", "Forward Link"),
("mention", "Mention"), ("page_mention", "Page Mention"),
("user_mention", "User Mention"),
) )
transaction = models.UUIDField(default=uuid.uuid4) transaction = models.UUIDField(default=uuid.uuid4)
page = models.ForeignKey( page = models.ForeignKey(

View File

@ -1 +1 @@
from .instance import InstanceOwnerPermission, InstanceAdminPermission from .instance import InstanceAdminPermission

View File

@ -5,20 +5,6 @@ from rest_framework.permissions import BasePermission
from plane.license.models import Instance, InstanceAdmin from plane.license.models import Instance, InstanceAdmin
class InstanceOwnerPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
instance = Instance.objects.first()
return InstanceAdmin.objects.filter(
role=20,
instance=instance,
user=request.user,
).exists()
class InstanceAdminPermission(BasePermission): class InstanceAdminPermission(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):

View File

@ -2,7 +2,7 @@
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration
from plane.app.serializers import BaseSerializer from plane.app.serializers import BaseSerializer
from plane.app.serializers import UserAdminLiteSerializer from plane.app.serializers import UserAdminLiteSerializer
from plane.license.utils.encryption import decrypt_data
class InstanceSerializer(BaseSerializer): class InstanceSerializer(BaseSerializer):
primary_owner_details = UserAdminLiteSerializer(source="primary_owner", read_only=True) primary_owner_details = UserAdminLiteSerializer(source="primary_owner", read_only=True)
@ -12,14 +12,13 @@ class InstanceSerializer(BaseSerializer):
fields = "__all__" fields = "__all__"
read_only_fields = [ read_only_fields = [
"id", "id",
"primary_owner",
"primary_email",
"instance_id", "instance_id",
"license_key", "license_key",
"api_key", "api_key",
"version", "version",
"email", "email",
"last_checked_at", "last_checked_at",
"is_setup_done",
] ]
@ -40,3 +39,11 @@ class InstanceConfigurationSerializer(BaseSerializer):
class Meta: class Meta:
model = InstanceConfiguration model = InstanceConfiguration
fields = "__all__" fields = "__all__"
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:
data["value"] = decrypt_data(instance.value)
return data

View File

@ -1,6 +1,9 @@
from .instance import ( from .instance import (
InstanceEndpoint, InstanceEndpoint,
TransferPrimaryOwnerEndpoint,
InstanceAdminEndpoint, InstanceAdminEndpoint,
InstanceConfigurationEndpoint, InstanceConfigurationEndpoint,
AdminSetupMagicSignInEndpoint,
SignUpScreenVisitedEndpoint,
AdminMagicSignInGenerateEndpoint,
AdminSetUserPasswordEndpoint,
) )

View File

@ -2,13 +2,21 @@
import json import json
import os import os
import requests import requests
import uuid
import random
import string
# Django imports # Django imports
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.hashers import make_password
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
# Module imports # Module imports
from plane.app.views import BaseAPIView from plane.app.views import BaseAPIView
@ -18,24 +26,26 @@ from plane.license.api.serializers import (
InstanceAdminSerializer, InstanceAdminSerializer,
InstanceConfigurationSerializer, InstanceConfigurationSerializer,
) )
from plane.app.serializers import UserSerializer
from plane.license.api.permissions import ( from plane.license.api.permissions import (
InstanceOwnerPermission,
InstanceAdminPermission, InstanceAdminPermission,
) )
from plane.db.models import User 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
from plane.license.utils.instance_value import get_configuration_value
class InstanceEndpoint(BaseAPIView): class InstanceEndpoint(BaseAPIView):
def get_permissions(self): def get_permissions(self):
if self.request.method in ["POST", "PATCH"]: if self.request.method == "PATCH":
self.permission_classes = [ return [
InstanceOwnerPermission, InstanceAdminPermission(),
] ]
else: return [
self.permission_classes = [ AllowAny(),
InstanceAdminPermission, ]
]
return super(InstanceEndpoint, self).get_permissions()
def post(self, request): def post(self, request):
# Check if the instance is registered # Check if the instance is registered
@ -58,12 +68,14 @@ class InstanceEndpoint(BaseAPIView):
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
payload = { payload = {
"email": request.user.email, "instance_key": os.environ.get("INSTANCE_KEY"),
"version": data.get("version", 0.1), "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( response = requests.post(
f"{license_engine_base_url}/api/instances", f"{license_engine_base_url}/api/instances/",
headers=headers, headers=headers,
data=json.dumps(payload), data=json.dumps(payload),
) )
@ -77,21 +89,15 @@ class InstanceEndpoint(BaseAPIView):
license_key=data.get("license_key"), license_key=data.get("license_key"),
api_key=data.get("api_key"), api_key=data.get("api_key"),
version=data.get("version"), version=data.get("version"),
primary_email=data.get("email"),
primary_owner=request.user,
last_checked_at=timezone.now(), last_checked_at=timezone.now(),
) user_count=data.get("user_count", 0),
# Create instance admin
_ = InstanceAdmin.objects.create(
user=request.user,
instance=instance,
role=20,
) )
serializer = InstanceSerializer(instance)
data = serializer.data
data["is_activated"] = True
return Response( return Response(
{ data,
"message": f"Instance succesfully registered with owner: {instance.primary_owner.email}"
},
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
return Response( return Response(
@ -100,9 +106,7 @@ class InstanceEndpoint(BaseAPIView):
) )
else: else:
return Response( return Response(
{ {"message": "Instance already registered"},
"message": f"Instance already registered with instance owner: {instance.primary_owner.email}"
},
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
@ -110,11 +114,15 @@ class InstanceEndpoint(BaseAPIView):
instance = Instance.objects.first() instance = Instance.objects.first()
# get the instance # get the instance
if instance is None: if instance is None:
return Response({"activated": False}, status=status.HTTP_400_BAD_REQUEST) return Response(
{"is_activated": False, "is_setup_done": False},
status=status.HTTP_400_BAD_REQUEST,
)
# Return instance # Return instance
serializer = InstanceSerializer(instance) serializer = InstanceSerializer(instance)
serializer.data["activated"] = True data = serializer.data
return Response(serializer.data, status=status.HTTP_200_OK) data["is_activated"] = True
return Response(data, status=status.HTTP_200_OK)
def patch(self, request): def patch(self, request):
# Get the instance # Get the instance
@ -126,58 +134,15 @@ class InstanceEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class TransferPrimaryOwnerEndpoint(BaseAPIView):
permission_classes = [
InstanceOwnerPermission,
]
# Transfer the owner of the instance
def post(self, request):
instance = Instance.objects.first()
# Get the email of the new user
email = request.data.get("email", False)
if not email:
return Response(
{"error": "User is required"}, status=status.HTTP_400_BAD_REQUEST
)
# Get users
user = User.objects.get(email=email)
# Save the instance user
instance.primary_owner = user
instance.primary_email = user.email
instance.save(update_fields=["owner", "email"])
# Add the user to admin
_ = InstanceAdmin.objects.get_or_create(
instance=instance,
user=user,
role=20,
)
return Response(
{"message": "Owner successfully updated"}, status=status.HTTP_200_OK
)
class InstanceAdminEndpoint(BaseAPIView): class InstanceAdminEndpoint(BaseAPIView):
def get_permissions(self): permission_classes = [
if self.request.method in ["POST", "DELETE"]: InstanceAdminPermission,
self.permission_classes = [ ]
InstanceOwnerPermission,
]
else:
self.permission_classes = [
InstanceAdminPermission,
]
return super(InstanceAdminEndpoint, self).get_permissions()
# Create an instance admin # Create an instance admin
def post(self, request): def post(self, request):
email = request.data.get("email", False) email = request.data.get("email", False)
role = request.data.get("role", 15) role = request.data.get("role", 20)
if not email: if not email:
return Response( return Response(
@ -230,18 +195,301 @@ class InstanceConfigurationEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request): def patch(self, request):
configurations = InstanceConfiguration.objects.filter(key__in=request.data.keys()) configurations = InstanceConfiguration.objects.filter(
key__in=request.data.keys()
)
bulk_configurations = [] bulk_configurations = []
for configuration in configurations: for configuration in configurations:
configuration.value = request.data.get(configuration.key, configuration.value) value = request.data.get(configuration.key, configuration.value)
if value is not None and configuration.key in [
"OPENAI_API_KEY",
"GITHUB_CLIENT_SECRET",
"EMAIL_HOST_PASSWORD",
"UNSPLASH_ACESS_KEY",
]:
configuration.value = encrypt_data(value)
else:
configuration.value = value
bulk_configurations.append(configuration) bulk_configurations.append(configuration)
InstanceConfiguration.objects.bulk_update( InstanceConfiguration.objects.bulk_update(
bulk_configurations, bulk_configurations, ["value"], batch_size=100
["value"],
batch_size=100
) )
serializer = InstanceConfigurationSerializer(configurations, many=True) serializer = InstanceConfigurationSerializer(configurations, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
return (
str(refresh.access_token),
str(refresh),
)
class AdminMagicSignInGenerateEndpoint(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:
return Response(
{"error": "Instance is not configured"},
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,
)
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)
password = request.data.get("password", False)
# If the user password is not autoset then return error
if not user.is_password_autoset:
return Response(
{
"error": "Your password is already set please change your password from profile"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Check password validation
if not password and len(str(password)) < 8:
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"},
status=status.HTTP_400_BAD_REQUEST,
)
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
if not license_engine_base_url:
return Response(
{"error": "License engine base url is required"},
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"{license_engine_base_url}/api/instances/",
headers=headers,
data=json.dumps({"is_setup_done": True}),
)
# Also register the user as admin
_ = requests.post(
f"{license_engine_base_url}/api/instances/users/register/",
headers=headers,
data=json.dumps(
{
"email": str(user.email),
"signup_mode": "MAGIC_CODE",
"is_admin": True,
}
),
)
# Register the user as an instance admin
_ = InstanceAdmin.objects.create(
user=user,
instance=instance,
)
# Make the setup flag True
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)
class SignUpScreenVisitedEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
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,
)
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
if not license_engine_base_url:
return Response(
{"error": "License engine base url is required"},
status=status.HTTP_400_BAD_REQUEST,
)
headers = {
"Content-Type": "application/json",
"x-instance-id": instance.instance_id,
"x-api-key": instance.api_key,
}
payload = {"is_signup_screen_visited": True}
response = requests.patch(
f"{license_engine_base_url}/api/instances/",
headers=headers,
data=json.dumps(payload),
)
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -2,48 +2,120 @@
import os import os
# Django imports # Django imports
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand
from django.utils import timezone from django.conf import settings
# Module imports # Module imports
from plane.license.models import InstanceConfiguration from plane.license.models import InstanceConfiguration
class Command(BaseCommand): class Command(BaseCommand):
help = "Configure instance variables" help = "Configure instance variables"
def handle(self, *args, **options): def handle(self, *args, **options):
config_keys = { from plane.license.utils.encryption import encrypt_data
# Authentication Settings
"GOOGLE_CLIENT_ID": os.environ.get("GOOGLE_CLIENT_ID"),
"GOOGLE_CLIENT_SECRET": os.environ.get("GOOGLE_CLIENT_SECRET"),
"GITHUB_CLIENT_ID": os.environ.get("GITHUB_CLIENT_ID"),
"GITHUB_CLIENT_SECRET": os.environ.get("GITHUB_CLIENT_SECRET"),
"ENABLE_SIGNUP": os.environ.get("ENABLE_SIGNUP", "1"),
"ENABLE_EMAIL_PASSWORD": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
"ENABLE_MAGIC_LINK_LOGIN": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"),
# Email Settings
"EMAIL_HOST": os.environ.get("EMAIL_HOST", ""),
"EMAIL_HOST_USER": os.environ.get("EMAIL_HOST_USER", ""),
"EMAIL_HOST_PASSWORD": os.environ.get("EMAIL_HOST_PASSWORD"),
"EMAIL_PORT": os.environ.get("EMAIL_PORT", "587"),
"EMAIL_FROM": os.environ.get("EMAIL_FROM", ""),
"EMAIL_USE_TLS": os.environ.get("EMAIL_USE_TLS", "1"),
"EMAIL_USE_SSL": os.environ.get("EMAIL_USE_SSL", "0"),
# Open AI Settings
"OPENAI_API_BASE": os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1"),
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", ""),
"GPT_ENGINE": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
# Unsplash Access Key
"UNSPLASH_ACCESS_KEY": os.environ.get("UNSPLASH_ACESS_KEY", "")
}
for key, value in config_keys.items(): config_keys = [
# Authentication Settings
{
"key": "ENABLE_SIGNUP",
"value": os.environ.get("ENABLE_SIGNUP", "1"),
"category": "AUTHENTICATION",
},
{
"key": "ENABLE_EMAIL_PASSWORD",
"value": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
"category": "AUTHENTICATION",
},
{
"key": "ENABLE_MAGIC_LINK_LOGIN",
"value": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"),
"category": "AUTHENTICATION",
},
{
"key": "GOOGLE_CLIENT_ID",
"value": os.environ.get("GOOGLE_CLIENT_ID"),
"category": "GOOGLE",
},
{
"key": "GITHUB_CLIENT_ID",
"value": os.environ.get("GITHUB_CLIENT_ID"),
"category": "GITHUB",
},
{
"key": "GITHUB_CLIENT_SECRET",
"value": encrypt_data(os.environ.get("GITHUB_CLIENT_SECRET"))
if os.environ.get("GITHUB_CLIENT_SECRET")
else None,
"category": "GITHUB",
},
{
"key": "EMAIL_HOST",
"value": os.environ.get("EMAIL_HOST", ""),
"category": "SMTP",
},
{
"key": "EMAIL_HOST_USER",
"value": os.environ.get("EMAIL_HOST_USER", ""),
"category": "SMTP",
},
{
"key": "EMAIL_HOST_PASSWORD",
"value": encrypt_data(os.environ.get("EMAIL_HOST_PASSWORD"))
if os.environ.get("EMAIL_HOST_PASSWORD")
else None,
"category": "SMTP",
},
{
"key": "EMAIL_PORT",
"value": os.environ.get("EMAIL_PORT", "587"),
"category": "SMTP",
},
{
"key": "EMAIL_FROM",
"value": os.environ.get("EMAIL_FROM", ""),
"category": "SMTP",
},
{
"key": "EMAIL_USE_TLS",
"value": os.environ.get("EMAIL_USE_TLS", "1"),
"category": "SMTP",
},
{
"key": "OPENAI_API_KEY",
"value": encrypt_data(os.environ.get("OPENAI_API_KEY"))
if os.environ.get("OPENAI_API_KEY")
else None,
"category": "OPENAI",
},
{
"key": "GPT_ENGINE",
"value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
"category": "SMTP",
},
{
"key": "UNSPLASH_ACCESS_KEY",
"value": encrypt_data(os.environ.get("UNSPLASH_ACESS_KEY", ""))
if os.environ.get("UNSPLASH_ACESS_KEY")
else None,
"category": "UNSPLASH",
},
]
for item in config_keys:
obj, created = InstanceConfiguration.objects.get_or_create( obj, created = InstanceConfiguration.objects.get_or_create(
key=key key=item.get("key")
) )
if created: if created:
obj.value = value obj.value = item.get("value")
obj.category = item.get("category")
obj.save() obj.save()
self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable.")) self.stdout.write(
self.style.SUCCESS(
f"{obj.key} loaded with value from environment variable."
)
)
else: else:
self.stdout.write(self.style.WARNING(f"{key} configuration already exists")) self.stdout.write(
self.style.WARNING(f"{obj.key} configuration already exists")
)

View File

@ -2,22 +2,23 @@
import json import json
import os import os
import requests import requests
import uuid
# Django imports # Django imports
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
# Module imports # Module imports
from plane.license.models import Instance
from plane.db.models import User from plane.db.models import User
from plane.license.models import Instance, InstanceAdmin
class Command(BaseCommand): class Command(BaseCommand):
help = "Check if instance in registered else register" help = "Check if instance in registered else register"
def add_arguments(self, parser):
# Positional argument
parser.add_argument('machine_signature', type=str, help='Machine signature')
def handle(self, *args, **options): def handle(self, *args, **options):
# Check if the instance is registered # Check if the instance is registered
instance = Instance.objects.first() instance = Instance.objects.first()
@ -28,25 +29,15 @@ class Command(BaseCommand):
# Load JSON content from the file # Load JSON content from the file
data = json.load(file) data = json.load(file)
admin_email = os.environ.get("ADMIN_EMAIL") machine_signature = options.get("machine_signature", False)
instance_key = os.environ.get("INSTANCE_KEY", False)
try:
validate_email(admin_email)
except ValidationError:
CommandError(f"{admin_email} is not a valid ADMIN_EMAIL")
# Raise an exception if the admin email is not provided # Raise an exception if the admin email is not provided
if not admin_email: if not instance_key:
raise CommandError("ADMIN_EMAIL is required") raise CommandError("INSTANCE_KEY is required")
# Check if the admin email user exists if not machine_signature:
user = User.objects.filter(email=admin_email).first() raise CommandError("Machine signature is required")
# If the user does not exist create the user and add him to the database
if user is None:
user = User.objects.create(email=admin_email, username=uuid.uuid4().hex)
user.set_password(admin_email)
user.save()
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL") license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
@ -56,8 +47,10 @@ class Command(BaseCommand):
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
payload = { payload = {
"email": user.email, "instance_key": instance_key,
"version": data.get("version", 0.1), "version": data.get("version", 0.1),
"machine_signature": machine_signature,
"user_count": User.objects.filter(is_bot=False).count(),
} }
response = requests.post( response = requests.post(
@ -75,20 +68,13 @@ class Command(BaseCommand):
license_key=data.get("license_key"), license_key=data.get("license_key"),
api_key=data.get("api_key"), api_key=data.get("api_key"),
version=data.get("version"), version=data.get("version"),
primary_email=data.get("email"),
primary_owner=user,
last_checked_at=timezone.now(), last_checked_at=timezone.now(),
) user_count=data.get("user_count", 0),
# Create instance admin
_ = InstanceAdmin.objects.create(
user=user,
instance=instance,
role=20,
) )
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f"Instance successfully registered with owner: {instance.primary_owner.email}" f"Instance successfully registered"
) )
) )
return return
@ -96,7 +82,7 @@ class Command(BaseCommand):
else: else:
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f"Instance already registered with instance owner: {instance.primary_owner.email}" f"Instance already registered"
) )
) )
return return

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.5 on 2023-11-15 14:22 # Generated by Django 4.2.7 on 2023-11-29 14:39
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -27,13 +27,14 @@ class Migration(migrations.Migration):
('license_key', models.CharField(blank=True, max_length=256, null=True)), ('license_key', models.CharField(blank=True, max_length=256, null=True)),
('api_key', models.CharField(max_length=16)), ('api_key', models.CharField(max_length=16)),
('version', models.CharField(max_length=10)), ('version', models.CharField(max_length=10)),
('primary_email', models.CharField(max_length=256)),
('last_checked_at', models.DateTimeField()), ('last_checked_at', models.DateTimeField()),
('namespace', models.CharField(blank=True, max_length=50, null=True)), ('namespace', models.CharField(blank=True, max_length=50, null=True)),
('is_telemetry_enabled', models.BooleanField(default=True)), ('is_telemetry_enabled', models.BooleanField(default=True)),
('is_support_required', models.BooleanField(default=True)), ('is_support_required', models.BooleanField(default=True)),
('is_setup_done', models.BooleanField(default=False)),
('is_signup_screen_visited', models.BooleanField(default=False)),
('user_count', models.PositiveBigIntegerField(default=0)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('primary_owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_primary_owner', to=settings.AUTH_USER_MODEL)),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
], ],
options={ options={
@ -51,6 +52,7 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('key', models.CharField(max_length=100, unique=True)), ('key', models.CharField(max_length=100, unique=True)),
('value', models.TextField(blank=True, default=None, null=True)), ('value', models.TextField(blank=True, default=None, null=True)),
('category', models.TextField()),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
], ],
@ -67,7 +69,7 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('role', models.PositiveIntegerField(choices=[(20, 'Owner'), (15, 'Admin')], default=15)), ('role', models.PositiveIntegerField(choices=[(20, 'Admin')], default=20)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admins', to='license.instance')), ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admins', to='license.instance')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
@ -78,6 +80,7 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Instance Admins', 'verbose_name_plural': 'Instance Admins',
'db_table': 'instance_admins', 'db_table': 'instance_admins',
'ordering': ('-created_at',), 'ordering': ('-created_at',),
'unique_together': {('instance', 'user')},
}, },
), ),
] ]

View File

@ -1,19 +0,0 @@
# Generated by Django 4.2.5 on 2023-11-16 09:45
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('license', '0001_initial'),
]
operations = [
migrations.AlterUniqueTogether(
name='instanceadmin',
unique_together={('instance', 'user')},
),
]

View File

@ -4,11 +4,9 @@ from django.conf import settings
# Module imports # Module imports
from plane.db.models import BaseModel from plane.db.models import BaseModel
from plane.db.mixins import AuditModel
ROLE_CHOICES = ( ROLE_CHOICES = (
(20, "Owner"), (20, "Admin"),
(15, "Admin"),
) )
@ -20,20 +18,18 @@ class Instance(BaseModel):
license_key = models.CharField(max_length=256, null=True, blank=True) license_key = models.CharField(max_length=256, null=True, blank=True)
api_key = models.CharField(max_length=16) api_key = models.CharField(max_length=16)
version = models.CharField(max_length=10) version = models.CharField(max_length=10)
# User information
primary_email = models.CharField(max_length=256)
primary_owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name="instance_primary_owner",
)
# Instnace specifics # Instnace specifics
last_checked_at = models.DateTimeField() last_checked_at = models.DateTimeField()
namespace = models.CharField(max_length=50, blank=True, null=True) namespace = models.CharField(max_length=50, blank=True, null=True)
# telemetry and support # telemetry and support
is_telemetry_enabled = models.BooleanField(default=True) is_telemetry_enabled = models.BooleanField(default=True)
is_support_required = models.BooleanField(default=True) is_support_required = models.BooleanField(default=True)
# is setup done
is_setup_done = models.BooleanField(default=False)
# signup screen
is_signup_screen_visited = models.BooleanField(default=False)
# users
user_count = models.PositiveBigIntegerField(default=0)
class Meta: class Meta:
verbose_name = "Instance" verbose_name = "Instance"
@ -50,7 +46,7 @@ class InstanceAdmin(BaseModel):
related_name="instance_owner", related_name="instance_owner",
) )
instance = models.ForeignKey(Instance, on_delete=models.CASCADE, related_name="admins") instance = models.ForeignKey(Instance, on_delete=models.CASCADE, related_name="admins")
role = models.PositiveIntegerField(choices=ROLE_CHOICES, default=15) role = models.PositiveIntegerField(choices=ROLE_CHOICES, default=20)
class Meta: class Meta:
unique_together = ["instance", "user"] unique_together = ["instance", "user"]
@ -64,6 +60,7 @@ class InstanceConfiguration(BaseModel):
# The instance configuration variables # The instance configuration variables
key = models.CharField(max_length=100, unique=True) key = models.CharField(max_length=100, unique=True)
value = models.TextField(null=True, blank=True, default=None) value = models.TextField(null=True, blank=True, default=None)
category = models.TextField()
class Meta: class Meta:
verbose_name = "Instance Configuration" verbose_name = "Instance Configuration"

View File

@ -2,9 +2,12 @@ from django.urls import path
from plane.license.api.views import ( from plane.license.api.views import (
InstanceEndpoint, InstanceEndpoint,
TransferPrimaryOwnerEndpoint,
InstanceAdminEndpoint, InstanceAdminEndpoint,
InstanceConfigurationEndpoint, InstanceConfigurationEndpoint,
AdminMagicSignInGenerateEndpoint,
AdminSetupMagicSignInEndpoint,
AdminSetUserPasswordEndpoint,
SignUpScreenVisitedEndpoint,
) )
urlpatterns = [ urlpatterns = [
@ -13,11 +16,6 @@ urlpatterns = [
InstanceEndpoint.as_view(), InstanceEndpoint.as_view(),
name="instance", name="instance",
), ),
path(
"instances/transfer-primary-owner/",
TransferPrimaryOwnerEndpoint.as_view(),
name="instance",
),
path( path(
"instances/admins/", "instances/admins/",
InstanceAdminEndpoint.as_view(), InstanceAdminEndpoint.as_view(),
@ -33,4 +31,24 @@ urlpatterns = [
InstanceConfigurationEndpoint.as_view(), InstanceConfigurationEndpoint.as_view(),
name="instance-configuration", 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",
),
path(
"instances/admins/sign-up-screen-visited/",
SignUpScreenVisitedEndpoint.as_view(),
name="instance-sign-up",
),
] ]

View File

@ -0,0 +1,22 @@
import base64
import hashlib
from django.conf import settings
from cryptography.fernet import Fernet
def derive_key(secret_key):
# Use a key derivation function to get a suitable encryption key
dk = hashlib.pbkdf2_hmac('sha256', secret_key.encode(), b'salt', 100000)
return base64.urlsafe_b64encode(dk)
def encrypt_data(data):
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
encrypted_data = cipher_suite.encrypt(data.encode())
return encrypted_data.decode() # Convert bytes to string
def decrypt_data(encrypted_data):
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
decrypted_data = cipher_suite.decrypt(encrypted_data.encode()) # Convert string back to bytes
return decrypted_data.decode()

View File

@ -1,6 +1,63 @@
import os
# Helper function to return value from the passed key # Helper function to return value from the passed key
def get_configuration_value(query, key, default=None): def get_configuration_value(query, key, default=None):
for item in query: for item in query:
if item['key'] == key: if item["key"] == key:
return item.get("value", default) return item.get("value", default)
return default return default
def get_email_configuration(instance_configuration):
# Get the configuration variables
EMAIL_HOST_USER = get_configuration_value(
instance_configuration,
"EMAIL_HOST_USER",
os.environ.get("EMAIL_HOST_USER", None),
)
EMAIL_HOST_PASSWORD = get_configuration_value(
instance_configuration,
"EMAIL_HOST_PASSWORD",
os.environ.get("EMAIL_HOST_PASSWORD", None),
)
EMAIL_HOST = get_configuration_value(
instance_configuration,
"EMAIL_HOST",
os.environ.get("EMAIL_HOST", None),
)
EMAIL_FROM = get_configuration_value(
instance_configuration,
"EMAIL_FROM",
os.environ.get("EMAIL_FROM", None),
)
EMAIL_USE_TLS = get_configuration_value(
instance_configuration,
"EMAIL_USE_TLS",
os.environ.get("EMAIL_USE_TLS", "1"),
)
EMAIL_PORT = get_configuration_value(
instance_configuration,
"EMAIL_PORT",
587,
)
EMAIL_FROM = get_configuration_value(
instance_configuration,
"EMAIL_FROM",
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
)
return (
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_FROM,
)

View File

@ -185,6 +185,9 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Password reset time the number of seconds the uniquely generated uid will be valid
PASSWORD_RESET_TIMEOUT = 3600
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static-assets", "collected-static") STATIC_ROOT = os.path.join(BASE_DIR, "static-assets", "collected-static")
@ -306,7 +309,6 @@ if bool(os.environ.get("SENTRY_DSN", False)):
PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) # For External PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) # For External
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
# Unsplash Access key # Unsplash Access key
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY") UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")

View File

@ -36,4 +36,5 @@ scout-apm==2.26.1
openpyxl==3.1.2 openpyxl==3.1.2
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
dj-database-url==2.1.0 dj-database-url==2.1.0
posthog==3.0.2 posthog==3.0.2
cryptography==41.0.5

File diff suppressed because it is too large Load Diff