mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
dev: create migration and create endpoint separated for creating and getting connected accounts
This commit is contained in:
parent
ee90a22a0c
commit
49b32c2b46
@ -7,6 +7,7 @@ from .user import (
|
|||||||
UserAdminLiteSerializer,
|
UserAdminLiteSerializer,
|
||||||
UserMeSerializer,
|
UserMeSerializer,
|
||||||
UserMeSettingsSerializer,
|
UserMeSettingsSerializer,
|
||||||
|
ConnectedAccountSerializer,
|
||||||
)
|
)
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
WorkSpaceSerializer,
|
WorkSpaceSerializer,
|
||||||
|
@ -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",
|
||||||
|
]
|
||||||
|
@ -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
|
||||||
]
|
]
|
||||||
|
@ -18,6 +18,7 @@ from .user import (
|
|||||||
UpdateUserOnBoardedEndpoint,
|
UpdateUserOnBoardedEndpoint,
|
||||||
UpdateUserTourCompletedEndpoint,
|
UpdateUserTourCompletedEndpoint,
|
||||||
UserActivityEndpoint,
|
UserActivityEndpoint,
|
||||||
|
ConnectedAccountEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .oauth import OauthEndpoint
|
from .oauth import OauthEndpoint
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user