✨ Feat: [#72] 깃허브 관련 기능 구현 완료
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from common.models.baseModels import BaseModel
|
||||
from common.models.choiceModels import GenderChoices, CertificateCodeUseType
|
||||
from common.utils.codeManger import set_expire
|
||||
@@ -9,6 +10,9 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, Permis
|
||||
|
||||
from common.models.choiceModels import NotificationType
|
||||
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
def create_user(self, email, password, **kwargs):
|
||||
user = self.model(email = email, **kwargs)
|
||||
@@ -72,3 +76,14 @@ class User(BaseModel, AbstractBaseUser, PermissionsMixin):
|
||||
if self.banner_image:
|
||||
self.banner_image.delete(save=False)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
class GithubToken(models.Model):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='github_token', on_delete=models.CASCADE)
|
||||
access_token = models.CharField(max_length=255, blank=True, null=True)
|
||||
refresh_token = models.CharField(max_length=255, blank=True, null=True)
|
||||
scope = models.CharField(max_length=255, blank=True)
|
||||
token_type = models.CharField(max_length=32, default='bearer')
|
||||
expires_at = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
return self.expires_at and timezone.now() >= self.expires_at
|
||||
11
users/permissions.py
Normal file
11
users/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# permissions.py
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
class IsGithubLinked(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
user = request.user
|
||||
|
||||
if token_obj := getattr(user, 'github_token', None):
|
||||
if token := getattr(token_obj, 'access_token', None):
|
||||
return True
|
||||
return False
|
||||
@@ -74,3 +74,7 @@ class UserMemberInfoSerializer(serializers.ModelSerializer):
|
||||
'custom_url',
|
||||
'job_and_interests'
|
||||
]
|
||||
|
||||
|
||||
class GitHubCodeSerializer(serializers.Serializer):
|
||||
code = serializers.CharField()
|
||||
@@ -1,3 +1,5 @@
|
||||
import requests
|
||||
|
||||
from .models import *
|
||||
from projects.models import *
|
||||
from portfolios.models import *
|
||||
@@ -5,6 +7,8 @@ from portfolios.models import *
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
DUPLICATE_CHECK = {
|
||||
'email': 'email',
|
||||
@@ -12,6 +16,8 @@ DUPLICATE_CHECK = {
|
||||
'phone': 'phone'
|
||||
}
|
||||
|
||||
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
||||
|
||||
class CheckUserFieldDuplicateService:
|
||||
@staticmethod
|
||||
def check_duplicate(field: str, value: str) -> bool:
|
||||
@@ -92,3 +98,35 @@ class UserToProjectService:
|
||||
@staticmethod
|
||||
def get_scrap_project(user: User):
|
||||
return user.scrapped_projects.filter(is_published=True)
|
||||
|
||||
|
||||
class GithubTokenService:
|
||||
@staticmethod
|
||||
def exchange_code_to_access_token(code: str) -> dict:
|
||||
try:
|
||||
resp = requests.post(
|
||||
GITHUB_TOKEN_URL,
|
||||
headers={"Accept": "application/json"},
|
||||
data = {
|
||||
"client_id": settings.GITHUB_CLIENT_ID,
|
||||
"client_secret": settings.GITHUB_CLIENT_SECRET,
|
||||
"code": code,
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except requests.HTTPError as e:
|
||||
raise Exception(
|
||||
f"Github token exchange failed: {e.response.status_code} - {e.response.text}"
|
||||
)
|
||||
|
||||
def github_token_save(data: dict, user: User):
|
||||
return GithubToken.objects.update_or_create(
|
||||
user=user,
|
||||
defaults={
|
||||
"access_token": data["access_token"],
|
||||
"scope": data.get("scope", ""),
|
||||
"token_type": data.get("token_type", "bearer"),
|
||||
},
|
||||
)
|
||||
@@ -1,7 +1,12 @@
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from .views import *
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
router = DefaultRouter(trailing_slash=True)
|
||||
router.register(r"action", GithubAPIViewSet, basename="github")
|
||||
|
||||
|
||||
app_name = 'users'
|
||||
|
||||
@@ -17,4 +22,5 @@ urlpatterns = [
|
||||
path('mypage/profile/<str:nickname>/', MyPageProfileAPIView.as_view()),
|
||||
path('mypage/works/<str:nickname>/', MyPageWorkListAPIView.as_view()),
|
||||
path('mypage/my-info/', MyPageMemberInfoAPIView.as_view()),
|
||||
path("github/", include(router.urls)),
|
||||
]
|
||||
124
users/views.py
124
users/views.py
@@ -8,16 +8,20 @@ from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, Toke
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from rest_framework_simplejwt.exceptions import TokenError, InvalidToken
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.db.models import Case, When, Value, IntegerField, Q
|
||||
from django.utils.functional import cached_property
|
||||
from django.core.cache import cache
|
||||
|
||||
from .models import *
|
||||
from .serializers import *
|
||||
from .services import *
|
||||
from .permissions import *
|
||||
|
||||
from common.utils.fileManager import file_delete
|
||||
|
||||
@@ -29,6 +33,10 @@ from django.db import transaction
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.transport import requests
|
||||
|
||||
from github import Github, GithubException
|
||||
|
||||
CACHE_TIMEOUT = 60 * 60
|
||||
COMMIT_PAGE_SIZE = 20
|
||||
|
||||
class RefreshAPIView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
@@ -107,7 +115,121 @@ class GoogleLoginAPIView(APIView):
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
class GithubAPIViewSet(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated, IsGithubLinked]
|
||||
# 깃허브 연동 및 api 요청
|
||||
|
||||
def _get_github(self):
|
||||
uid = self.request.user.id
|
||||
cache_key = f"gh-token:{uid}"
|
||||
|
||||
token = cache.get(cache_key)
|
||||
|
||||
if token is None:
|
||||
token = self.request.user.github_token.access_token
|
||||
cache.set(cache_key, token, timeout=CACHE_TIMEOUT)
|
||||
|
||||
return Github(token)
|
||||
|
||||
@cached_property
|
||||
def github(self):
|
||||
return self._get_github()
|
||||
|
||||
@transaction.atomic
|
||||
@action(detail=False, methods=["post"], url_path="connect", permission_classes=[IsAuthenticated])
|
||||
def connect(self, request):
|
||||
serializer = GithubCodeSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
try:
|
||||
data = GithubTokenService.exchange_code_to_access_token(serializer.validated_data['code'])
|
||||
except Exception as e:
|
||||
return Response({"connected": False, "detail": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
GithubTokenService.github_token_save(data, request.user)
|
||||
return Response({"connected": True}, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="personal-repos")
|
||||
def personal_repos(self, request):
|
||||
user = self.github.get_user()
|
||||
data = [
|
||||
{'full_name': r.full_name}
|
||||
for r in user.get_repos()
|
||||
]
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="commits")
|
||||
def commits(self, request):
|
||||
repo_full_name = request.query_params.get("repo")
|
||||
if not repo_full_name:
|
||||
return Response({"is_query_param": False}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
page = int(request.query_params.get("page", 1))
|
||||
except ValueError:
|
||||
return Response({"is_page_int": False,"detail": "page and page_size must be integers"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if page < 1:
|
||||
return Response({"is_page_gte_1": False, "detail": "page and page_size must be >= 1"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
repo = self.github.get_repo(repo_full_name)
|
||||
commits = repo.get_commits(per_page=COMMIT_PAGE_SIZE).get_page(page - 1)
|
||||
except Exception as e:
|
||||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
data = [
|
||||
{"sha": c.sha, "commit_message": c.commit.message}
|
||||
for c in commits[:COMMIT_PAGE_SIZE]
|
||||
]
|
||||
|
||||
return Response({
|
||||
"page": page,
|
||||
"page_size": COMMIT_PAGE_SIZE,
|
||||
"results": data
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="commit-files")
|
||||
def commit_files(self, request):
|
||||
repo_full, sha = request.query_params.get("repo"), request.query_params.get("sha")
|
||||
if not (repo_full and sha):
|
||||
return Response({"is_query_param": False}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
commit = self.github.get_repo(repo_full).get_commit(sha)
|
||||
return Response([{"filename": f.filename, "status": f.status} for f in commit.files])
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="file-content")
|
||||
def file_content(self, request):
|
||||
repo_full = request.query_params.get("repo")
|
||||
filename = request.query_params.get("filename")
|
||||
sha = request.query_params.get("sha")
|
||||
if not (repo_full and filename and sha):
|
||||
return Response({"is_query_param": False}, status=status.HTTP_400_BAD_REQUEST)
|
||||
try:
|
||||
file = self.github.get_repo(repo_full).get_contents(path=filename, ref=sha)
|
||||
if file.encoding == "base64":
|
||||
content = file.decoded_content.decode("utf-8", errors="replace")
|
||||
else:
|
||||
content = file.content
|
||||
return Response({"path": file.path, "ref": sha, "content": content})
|
||||
except GithubException as e:
|
||||
return Response({"detail": str(e)}, status=e.status)
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="organizations")
|
||||
def organizations(self, request):
|
||||
user = self.github.get_user()
|
||||
orgs = user.get_orgs()
|
||||
data = {"org_name": [org.name for org in orgs]}
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="organization-repos")
|
||||
def organization_repos(self, request):
|
||||
org_name = request.query_params.get("org")
|
||||
repos = self.github.get_organization(org_name).get_repos()
|
||||
|
||||
data = [
|
||||
{'full_name': r.full_name}
|
||||
for r in repos
|
||||
]
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class JoinAPIView(APIView):
|
||||
|
||||
Reference in New Issue
Block a user