mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
7 Commits
feat/inbox
...
feat/conne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19c7281891 | ||
|
|
363166f491 | ||
|
|
49b32c2b46 | ||
|
|
ee90a22a0c | ||
|
|
5756555507 | ||
|
|
5ed4fa20d3 | ||
|
|
f88d63bdff |
@@ -7,6 +7,7 @@ from .user import (
|
||||
UserAdminLiteSerializer,
|
||||
UserMeSerializer,
|
||||
UserMeSettingsSerializer,
|
||||
ConnectedAccountSerializer,
|
||||
)
|
||||
from .workspace import (
|
||||
WorkSpaceSerializer,
|
||||
|
||||
@@ -3,7 +3,7 @@ from rest_framework import serializers
|
||||
|
||||
# Module import
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import User, Workspace, WorkspaceMemberInvite
|
||||
from plane.db.models import User, Workspace, WorkspaceMemberInvite, ConnectedAccount
|
||||
from plane.license.models import InstanceAdmin, Instance
|
||||
|
||||
|
||||
@@ -190,4 +190,15 @@ class ResetPasswordSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for password change endpoint.
|
||||
"""
|
||||
|
||||
new_password = serializers.CharField(required=True, min_length=8)
|
||||
|
||||
|
||||
class ConnectedAccountSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = ConnectedAccount
|
||||
fields = "__all__"
|
||||
read_only_field = [
|
||||
"user",
|
||||
"access_token",
|
||||
]
|
||||
|
||||
@@ -8,6 +8,7 @@ from plane.app.views import (
|
||||
UserActivityEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
ConnectedAccountEndpoint,
|
||||
## End User
|
||||
## Workspaces
|
||||
UserWorkSpacesEndpoint,
|
||||
@@ -95,5 +96,15 @@ urlpatterns = [
|
||||
SetUserPasswordEndpoint.as_view(),
|
||||
name="set-password",
|
||||
),
|
||||
path(
|
||||
"users/me/connected-accounts/",
|
||||
ConnectedAccountEndpoint.as_view(),
|
||||
name="connected-account",
|
||||
),
|
||||
path(
|
||||
"users/me/connected-accounts/<str:medium>/",
|
||||
ConnectedAccountEndpoint.as_view(),
|
||||
name="connected-account",
|
||||
),
|
||||
## End User Graph
|
||||
]
|
||||
|
||||
@@ -18,6 +18,7 @@ from .user import (
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
ConnectedAccountEndpoint,
|
||||
)
|
||||
|
||||
from .oauth import OauthEndpoint
|
||||
|
||||
@@ -6,6 +6,7 @@ import os
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
# Third Party modules
|
||||
from rest_framework.response import Response
|
||||
@@ -27,6 +28,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
ProjectMemberInvite,
|
||||
ProjectMember,
|
||||
ConnectedAccount,
|
||||
)
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
from .base import BaseAPIView
|
||||
@@ -95,15 +97,15 @@ def get_access_token(request_token: str, client_id: str) -> str:
|
||||
]
|
||||
)
|
||||
|
||||
url = f"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={CLIENT_SECRET}&code={request_token}"
|
||||
url = f"https://github.com/login/oauth/access_token?client_id={str(client_id)}&client_secret={str(CLIENT_SECRET)}&code={str(request_token)}"
|
||||
|
||||
headers = {"accept": "application/json"}
|
||||
|
||||
res = requests.post(url, headers=headers)
|
||||
|
||||
data = res.json()
|
||||
access_token = data["access_token"]
|
||||
|
||||
return access_token
|
||||
return data
|
||||
|
||||
|
||||
def get_user_data(access_token: str) -> dict:
|
||||
@@ -141,311 +143,263 @@ class OauthEndpoint(BaseAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
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)
|
||||
id_token = request.data.get("credential", False)
|
||||
client_id = request.data.get("clientId", False)
|
||||
|
||||
GOOGLE_CLIENT_ID, GITHUB_CLIENT_ID = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GOOGLE_CLIENT_ID",
|
||||
"default": os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_ID",
|
||||
"default": os.environ.get("GITHUB_CLIENT_ID"),
|
||||
},
|
||||
]
|
||||
# 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 not medium or not id_token:
|
||||
# Get the medium and temporary code
|
||||
medium = request.data.get("medium", False)
|
||||
id_token = request.data.get("credential", False)
|
||||
|
||||
GOOGLE_CLIENT_ID, GITHUB_CLIENT_ID = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GOOGLE_CLIENT_ID",
|
||||
"default": os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_ID",
|
||||
"default": os.environ.get("GITHUB_CLIENT_ID"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Return error if medium and id_token are not preset
|
||||
if not medium or not id_token:
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if medium == "google":
|
||||
if not GOOGLE_CLIENT_ID:
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
{"error": "Google login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
data = validate_google_token(id_token, GOOGLE_CLIENT_ID)
|
||||
|
||||
if medium == "github":
|
||||
if not GITHUB_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Github login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
account_data = get_access_token(id_token, GITHUB_CLIENT_ID)
|
||||
# access token authentication
|
||||
access_token = account_data.get("access_token", False)
|
||||
if not access_token:
|
||||
return Response(
|
||||
{"error": "Invalid credentials used"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if medium == "google":
|
||||
if not GOOGLE_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Google login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
data = validate_google_token(id_token, client_id)
|
||||
data = get_user_data(access_token=access_token)
|
||||
|
||||
if medium == "github":
|
||||
if not GITHUB_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Github login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
access_token = get_access_token(id_token, client_id)
|
||||
data = get_user_data(access_token)
|
||||
email = data.get("email", None)
|
||||
if email is None:
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
email = data.get("email", None)
|
||||
if email is None:
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if "@" in email:
|
||||
if "@" in email:
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
email = data["email"]
|
||||
mobile_number = uuid.uuid4().hex
|
||||
email_verified = True
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
user.is_active = True
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
user.last_login_medium = "oauth"
|
||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||
user.is_email_verified = email_verified
|
||||
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()
|
||||
|
||||
SocialLoginConnection.objects.update_or_create(
|
||||
medium=medium,
|
||||
extra_data={},
|
||||
user=user,
|
||||
defaults={
|
||||
"token_data": {"id_token": id_token},
|
||||
"last_login_at": timezone.now(),
|
||||
},
|
||||
)
|
||||
|
||||
# Send event
|
||||
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=medium.upper(),
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
except User.DoesNotExist:
|
||||
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"default": os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
}
|
||||
]
|
||||
)
|
||||
if (
|
||||
ENABLE_SIGNUP == "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
# Send event
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium=medium.upper(),
|
||||
first_time=False,
|
||||
)
|
||||
except User.DoesNotExist:
|
||||
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"default": os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
}
|
||||
]
|
||||
)
|
||||
if (
|
||||
ENABLE_SIGNUP == "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(
|
||||
username=uuid.uuid4().hex,
|
||||
email=email,
|
||||
mobile_number=mobile_number,
|
||||
first_name=data.get("first_name", ""),
|
||||
last_name=data.get("last_name", ""),
|
||||
is_email_verified=email_verified,
|
||||
is_password_autoset=True,
|
||||
password=make_password(uuid.uuid4().hex),
|
||||
)
|
||||
# Send event
|
||||
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=medium.upper(),
|
||||
first_time=True,
|
||||
)
|
||||
|
||||
username = uuid.uuid4().hex
|
||||
|
||||
if "@" in email:
|
||||
email = data["email"]
|
||||
mobile_number = uuid.uuid4().hex
|
||||
email_verified = True
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
user = User.objects.create(
|
||||
username=username,
|
||||
email=email,
|
||||
mobile_number=mobile_number,
|
||||
first_name=data.get("first_name", ""),
|
||||
last_name=data.get("last_name", ""),
|
||||
is_email_verified=email_verified,
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
||||
user.set_password(uuid.uuid4().hex)
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
user.last_login_medium = "oauth"
|
||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||
user.token_updated_at = timezone.now()
|
||||
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
|
||||
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=medium.upper(),
|
||||
first_time=True,
|
||||
)
|
||||
|
||||
SocialLoginConnection.objects.update_or_create(
|
||||
medium=medium,
|
||||
extra_data={},
|
||||
user=user,
|
||||
defaults={
|
||||
"token_data": {"id_token": id_token},
|
||||
"last_login_at": timezone.now(),
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
data = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
if medium == "github":
|
||||
# Get the values from the tokens
|
||||
(
|
||||
github_access_token,
|
||||
github_refresh_token,
|
||||
access_token_expired_at,
|
||||
refresh_token_expired_at,
|
||||
) = (
|
||||
account_data.get("access_token"),
|
||||
account_data.get("refresh_token", None),
|
||||
account_data.get("expires_in", None),
|
||||
account_data.get("refresh_token_expires_in", None),
|
||||
)
|
||||
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
# Get the connected account
|
||||
connected_account = ConnectedAccount.objects.filter(
|
||||
user=user, medium=medium
|
||||
).first()
|
||||
|
||||
if access_token_expired_at:
|
||||
access_token_expired_at = timezone.now() + timezone.timedelta(seconds=access_token_expired_at)
|
||||
refresh_token_expired_at = timezone.now() + timezone.timedelta(seconds=refresh_token_expired_at)
|
||||
|
||||
# If the connected account exists
|
||||
if connected_account:
|
||||
connected_account.access_token = github_access_token
|
||||
connected_account.refresh_token = github_refresh_token
|
||||
connected_account.access_token_expired_at = access_token_expired_at
|
||||
connected_account.refresh_token_expired_at = refresh_token_expired_at
|
||||
connected_account.last_connected_at = timezone.now()
|
||||
connected_account.save()
|
||||
else:
|
||||
# Create the connected account
|
||||
ConnectedAccount.objects.create(
|
||||
medium=medium,
|
||||
user=user,
|
||||
access_token=github_access_token,
|
||||
refresh_token=github_refresh_token,
|
||||
access_token_expired_at=access_token_expired_at,
|
||||
refresh_token_expired_at=refresh_token_expired_at,
|
||||
last_connected_at=timezone.now(),
|
||||
)
|
||||
|
||||
user.is_active = True
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
user.last_login_medium = "oauth"
|
||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||
user.is_email_verified = email_verified
|
||||
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()
|
||||
|
||||
SocialLoginConnection.objects.update_or_create(
|
||||
medium=medium,
|
||||
extra_data={},
|
||||
user=user,
|
||||
defaults={
|
||||
"token_data": {"id_token": id_token},
|
||||
"last_login_at": timezone.now(),
|
||||
},
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
# Python import
|
||||
import os
|
||||
import requests
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.app.serializers import (
|
||||
UserSerializer,
|
||||
IssueActivitySerializer,
|
||||
UserMeSerializer,
|
||||
UserMeSettingsSerializer,
|
||||
ConnectedAccountSerializer,
|
||||
)
|
||||
|
||||
from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember
|
||||
from plane.db.models import (
|
||||
User,
|
||||
IssueActivity,
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
ConnectedAccount,
|
||||
)
|
||||
from plane.license.models import Instance, InstanceAdmin
|
||||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
|
||||
from django.db.models import Q, F, Count, Case, When, IntegerField
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
class UserEndpoint(BaseViewSet):
|
||||
@@ -51,7 +63,12 @@ class UserEndpoint(BaseViewSet):
|
||||
|
||||
# Instance admin check
|
||||
if InstanceAdmin.objects.filter(user=user).exists():
|
||||
return Response({"error": "You cannot deactivate your account since you are an instance admin"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot deactivate your account since you are an instance admin"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
projects_to_deactivate = []
|
||||
workspaces_to_deactivate = []
|
||||
@@ -159,3 +176,117 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||
).data,
|
||||
)
|
||||
|
||||
|
||||
class ConnectedAccountEndpoint(BaseAPIView):
|
||||
def get_access_token(self, request_token: str) -> str:
|
||||
"""Obtain the request token from github.
|
||||
Given the client id, client secret and request issued out by GitHub, this method
|
||||
should give back an access token
|
||||
Parameters
|
||||
----------
|
||||
CLIENT_ID: str
|
||||
A string representing the client id issued out by github
|
||||
CLIENT_SECRET: str
|
||||
A string representing the client secret issued out by github
|
||||
request_token: str
|
||||
A string representing the request token issued out by github
|
||||
Throws
|
||||
------
|
||||
ValueError:
|
||||
if CLIENT_ID or CLIENT_SECRET or request_token is empty or not a string
|
||||
Returns
|
||||
-------
|
||||
access_token: str
|
||||
A string representing the access token issued out by github
|
||||
"""
|
||||
|
||||
if not request_token:
|
||||
raise ValueError("The request token has to be supplied!")
|
||||
|
||||
(CLIENT_SECRET, GITHUB_CLIENT_ID) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GITHUB_CLIENT_SECRET",
|
||||
"default": os.environ.get("GITHUB_CLIENT_SECRET", None),
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_ID",
|
||||
"default": os.environ.get("GITHUB_CLIENT_ID"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
url = f"https://github.com/login/oauth/access_token?client_id={str(GITHUB_CLIENT_ID)}&client_secret={str(CLIENT_SECRET)}&code={str(request_token)}"
|
||||
|
||||
headers = {"accept": "application/json"}
|
||||
|
||||
res = requests.post(url, headers=headers)
|
||||
|
||||
data = res.json()
|
||||
|
||||
return data
|
||||
|
||||
def post(self, request):
|
||||
# Get the medium and temporary code
|
||||
medium = request.data.get("medium", False)
|
||||
id_token = request.data.get("credential", False)
|
||||
|
||||
if medium == "github":
|
||||
account_data = self.get_access_token(id_token)
|
||||
# Get the values from the tokens
|
||||
(
|
||||
github_access_token,
|
||||
github_refresh_token,
|
||||
access_token_expired_at,
|
||||
refresh_token_expired_at,
|
||||
) = (
|
||||
account_data.get("access_token"),
|
||||
account_data.get("refresh_token", None),
|
||||
account_data.get("expires_in", None),
|
||||
account_data.get("refresh_token_expires_in", None),
|
||||
)
|
||||
# Get the connected account
|
||||
connected_account = ConnectedAccount.objects.filter(
|
||||
user=request.user, medium=medium
|
||||
).first()
|
||||
|
||||
if access_token_expired_at:
|
||||
access_token_expired_at = timezone.now() + timezone.timedelta(
|
||||
seconds=access_token_expired_at
|
||||
)
|
||||
refresh_token_expired_at = timezone.now() + timezone.timedelta(
|
||||
seconds=refresh_token_expired_at
|
||||
)
|
||||
|
||||
# If the connected account exists
|
||||
if connected_account:
|
||||
connected_account.access_token = github_access_token
|
||||
connected_account.refresh_token = github_refresh_token
|
||||
connected_account.access_token_expired_at = access_token_expired_at
|
||||
connected_account.refresh_token_expired_at = refresh_token_expired_at
|
||||
connected_account.last_connected_at = timezone.now()
|
||||
connected_account.save()
|
||||
else:
|
||||
# Create the connected account
|
||||
connected_account = ConnectedAccount.objects.create(
|
||||
medium=medium,
|
||||
user=request.user,
|
||||
access_token=github_access_token,
|
||||
refresh_token=github_refresh_token,
|
||||
access_token_expired_at=access_token_expired_at,
|
||||
refresh_token_expired_at=refresh_token_expired_at,
|
||||
last_connected_at=timezone.now(),
|
||||
)
|
||||
|
||||
serializer = ConnectedAccountSerializer(connected_account)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def get(self, request):
|
||||
connected_accounts = ConnectedAccount.objects.filter(user=request.user)
|
||||
serializer = ConnectedAccountSerializer(connected_accounts, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, medium):
|
||||
connected_account = ConnectedAccount.objects.get(medium=medium, user=request.user)
|
||||
connected_account.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
42
apiserver/plane/db/migrations/0051_connectedaccount.py
Normal file
42
apiserver/plane/db/migrations/0051_connectedaccount.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-18 11:11
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0050_user_use_case_alter_workspace_organization_size'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ConnectedAccount',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created 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)),
|
||||
('medium', models.CharField(choices=[('Google', 'google'), ('Github', 'github')], max_length=20)),
|
||||
('access_token', models.CharField(max_length=255)),
|
||||
('access_token_expired_at', models.DateTimeField(null=True)),
|
||||
('refresh_token', models.CharField(max_length=255, null=True)),
|
||||
('refresh_token_expired_at', models.DateTimeField(null=True)),
|
||||
('metadata', models.JSONField(default=dict)),
|
||||
('last_connected_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('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')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_connected_accounts', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ConnectedAccount',
|
||||
'verbose_name_plural': 'ConnectedAccounts',
|
||||
'db_table': 'connected_accounts',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('user', 'medium')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
from .base import BaseModel
|
||||
|
||||
from .user import User
|
||||
from .user import User, ConnectedAccount
|
||||
|
||||
from .workspace import (
|
||||
Workspace,
|
||||
|
||||
@@ -17,6 +17,9 @@ from sentry_sdk import capture_exception
|
||||
from slack_sdk import WebClient
|
||||
from slack_sdk.errors import SlackApiError
|
||||
|
||||
# Module imports
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
def get_default_onboarding():
|
||||
return {
|
||||
@@ -65,7 +68,9 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
|
||||
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
user_timezone = models.CharField(max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES)
|
||||
user_timezone = models.CharField(
|
||||
max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES
|
||||
)
|
||||
|
||||
last_active = models.DateTimeField(default=timezone.now, null=True)
|
||||
last_login_time = models.DateTimeField(null=True)
|
||||
@@ -124,6 +129,35 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
super(User, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class ConnectedAccount(BaseModel):
|
||||
# medium the account is connected to
|
||||
medium = models.CharField(
|
||||
max_length=20,
|
||||
choices=(
|
||||
("Google", "google"),
|
||||
("Github", "github"),
|
||||
),
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="user_connected_accounts",
|
||||
)
|
||||
access_token = models.CharField(max_length=255)
|
||||
access_token_expired_at = models.DateTimeField(null=True)
|
||||
refresh_token = models.CharField(max_length=255, null=True)
|
||||
refresh_token_expired_at = models.DateTimeField(null=True)
|
||||
metadata = models.JSONField(default=dict)
|
||||
last_connected_at = models.DateTimeField(default=timezone.now)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["user", "medium"]
|
||||
verbose_name = "ConnectedAccount"
|
||||
verbose_name_plural = "ConnectedAccounts"
|
||||
db_table = "connected_accounts"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def send_welcome_slack(sender, instance, created, **kwargs):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user