dev: create migration and create endpoint separated for creating and getting connected accounts

This commit is contained in:
pablohashescobar 2023-12-18 18:00:40 +05:30
parent ee90a22a0c
commit 49b32c2b46
7 changed files with 198 additions and 9 deletions

View File

@ -7,6 +7,7 @@ from .user import (
UserAdminLiteSerializer, UserAdminLiteSerializer,
UserMeSerializer, UserMeSerializer,
UserMeSettingsSerializer, UserMeSettingsSerializer,
ConnectedAccountSerializer,
) )
from .workspace import ( from .workspace import (
WorkSpaceSerializer, WorkSpaceSerializer,

View File

@ -3,7 +3,7 @@ from rest_framework import serializers
# Module import # Module import
from .base import BaseSerializer 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 from plane.license.models import InstanceAdmin, Instance
@ -190,4 +190,15 @@ class ResetPasswordSerializer(serializers.Serializer):
""" """
Serializer for password change endpoint. Serializer for password change endpoint.
""" """
new_password = serializers.CharField(required=True, min_length=8) new_password = serializers.CharField(required=True, min_length=8)
class ConnectedAccountSerializer(BaseSerializer):
class Meta:
model = ConnectedAccount
fields = "__all__"
read_only_field = [
"user",
"access_token",
]

View File

@ -8,6 +8,7 @@ from plane.app.views import (
UserActivityEndpoint, UserActivityEndpoint,
ChangePasswordEndpoint, ChangePasswordEndpoint,
SetUserPasswordEndpoint, SetUserPasswordEndpoint,
ConnectedAccountEndpoint,
## End User ## End User
## Workspaces ## Workspaces
UserWorkSpacesEndpoint, UserWorkSpacesEndpoint,
@ -95,5 +96,15 @@ urlpatterns = [
SetUserPasswordEndpoint.as_view(), SetUserPasswordEndpoint.as_view(),
name="set-password", 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 ## End User Graph
] ]

View File

@ -18,6 +18,7 @@ from .user import (
UpdateUserOnBoardedEndpoint, UpdateUserOnBoardedEndpoint,
UpdateUserTourCompletedEndpoint, UpdateUserTourCompletedEndpoint,
UserActivityEndpoint, UserActivityEndpoint,
ConnectedAccountEndpoint,
) )
from .oauth import OauthEndpoint from .oauth import OauthEndpoint

View File

@ -296,6 +296,10 @@ class OauthEndpoint(BaseAPIView):
user=user, medium=medium user=user, medium=medium
).first() ).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 the connected account exists
if connected_account: if connected_account:
connected_account.access_token = github_access_token connected_account.access_token = github_access_token
@ -307,6 +311,7 @@ class OauthEndpoint(BaseAPIView):
else: else:
# Create the connected account # Create the connected account
ConnectedAccount.objects.create( ConnectedAccount.objects.create(
medium=medium,
user=user, user=user,
access_token=github_access_token, access_token=github_access_token,
refresh_token=github_refresh_token, refresh_token=github_refresh_token,

View File

@ -1,20 +1,31 @@
# Python import # Python import
import os import os
import requests
# Django imports
from django.utils import timezone
# Third party imports # Third party imports
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
# Module imports # Module imports
from plane.app.serializers import ( from plane.app.serializers import (
UserSerializer, UserSerializer,
IssueActivitySerializer, IssueActivitySerializer,
UserMeSerializer, UserMeSerializer,
UserMeSettingsSerializer, UserMeSettingsSerializer,
ConnectedAccountSerializer,
) )
from plane.app.views.base import BaseViewSet, BaseAPIView 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.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, IntegerField from django.db.models import Q, F, Count, Case, When, IntegerField
@ -52,7 +63,12 @@ class UserEndpoint(BaseViewSet):
# Instance admin check # Instance admin check
if InstanceAdmin.objects.filter(user=user).exists(): 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 = [] projects_to_deactivate = []
workspaces_to_deactivate = [] workspaces_to_deactivate = []
@ -162,13 +178,115 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
class ConnectedAccountEndpoint(BaseAPIView): 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
"""
def post(self, request): if not request_token:
GITHUB_APP_CLIENT_ID, = get_configuration_value( raise ValueError("The request token has to be supplied!")
(CLIENT_SECRET, GITHUB_CLIENT_ID) = get_configuration_value(
[ [
{ {
"key": "GITHUB_APP_CLIENT_ID", "key": "GITHUB_CLIENT_SECRET",
"default": os.environ.get("GITHUB_APP_CLIENT_ID"), "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)

View 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')},
},
),
]