mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
fd5b7d20a8
commit
5ccc226498
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
@ -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"})
|
||||||
|
|
||||||
|
@ -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/",
|
||||||
|
@ -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
|
||||||
]
|
]
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
{
|
{
|
||||||
@ -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")
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -296,7 +287,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=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")
|
||||||
@ -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)
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
@ -121,6 +122,9 @@ def service_importer(service, importer_id):
|
|||||||
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):
|
||||||
name = importer.metadata.get("name", False)
|
name = importer.metadata.get("name", False)
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
49
apiserver/plane/bgtasks/user_count_task.py
Normal file
49
apiserver/plane/bgtasks/user_count_task.py
Normal 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)
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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')),
|
||||||
|
@ -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),
|
||||||
]
|
]
|
||||||
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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]),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -1 +1 @@
|
|||||||
from .instance import InstanceOwnerPermission, InstanceAdminPermission
|
from .instance import InstanceAdminPermission
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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
|
@ -1,6 +1,9 @@
|
|||||||
from .instance import (
|
from .instance import (
|
||||||
InstanceEndpoint,
|
InstanceEndpoint,
|
||||||
TransferPrimaryOwnerEndpoint,
|
|
||||||
InstanceAdminEndpoint,
|
InstanceAdminEndpoint,
|
||||||
InstanceConfigurationEndpoint,
|
InstanceConfigurationEndpoint,
|
||||||
|
AdminSetupMagicSignInEndpoint,
|
||||||
|
SignUpScreenVisitedEndpoint,
|
||||||
|
AdminMagicSignInGenerateEndpoint,
|
||||||
|
AdminSetUserPasswordEndpoint,
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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')},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -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')},
|
|
||||||
),
|
|
||||||
]
|
|
@ -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"
|
||||||
|
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
22
apiserver/plane/license/utils/encryption.py
Normal file
22
apiserver/plane/license/utils/encryption.py
Normal 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()
|
@ -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,
|
||||||
|
)
|
||||||
|
@ -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")
|
||||||
|
@ -37,3 +37,4 @@ 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
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user