name: github-integration description: "Use this skill when the user wants to integrate GitHub OAuth authentication, create repositories programmatically, commit files to GitHub, or set up automatic repository creation for projects. Triggers include: mentions of 'GitHub integration', 'GitHub OAuth', 'create GitHub repo', 'commit to GitHub', 'GitHub API', 'repository automation', or requests to connect a Django/Next.js application with GitHub. Also use when implementing social authentication with GitHub, managing GitHub tokens securely, or building features that automatically push code to GitHub repositories. This skill covers both backend (Django with django-allauth and PyGithub) and frontend (Next.js/React) implementation." license: MIT
GitHub Integration for Django + Next.js Applications
Overview
Complete GitHub integration including OAuth authentication, repository management, and automatic code commits. Built with Django (backend) and Next.js (frontend).
Tech Stack
- Backend: Django, django-allauth, PyGithub, cryptography
- Frontend: Next.js, TypeScript, React
- Authentication: GitHub OAuth 2.0
- API: GitHub REST API v3
Quick Reference
| Task | Approach |
|---|---|
| OAuth Setup | django-allauth + GitHub OAuth App |
| Repository Creation | PyGithub client wrapper |
| File Commits | GitHub API with base64 encoding |
| Token Security | Fernet encryption |
| Frontend Auth | OAuth callback handling |
Backend Implementation
1. Dependencies
# requirements.txt
Django>=4.2.0
djangorestframework>=3.14.0
django-allauth>=0.57.0
PyGithub>=2.1.1
cryptography>=41.0.0
django-cors-headers>=4.3.0
python-dotenv>=1.0.0
2. Django Settings Configuration
# config/settings.py
from pathlib import Path
import os
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env')
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third party apps
"rest_framework",
"corsheaders",
"django.contrib.sites", # Required for allauth
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.github", # GitHub provider
# Local apps
"projects",
"agents",
"tasks",
"github_integration",
]
SITE_ID = 1
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware", # Must be before CommonMiddleware
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware", # Required for allauth
]
# CORS Settings
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
CORS_ALLOW_CREDENTIALS = True
# CSRF Settings
CSRF_TRUSTED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
# Session Settings
SESSION_COOKIE_SAMESITE = None # Allow cross-origin
SESSION_COOKIE_SECURE = False # Set to True in production with HTTPS
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = None
CSRF_COOKIE_SECURE = False # Set to True in production with HTTPS
CSRF_COOKIE_HTTPONLY = False # Allow JavaScript to read CSRF cookie
# REST Framework Settings
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
# Django Allauth Settings
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
# Redirect to frontend after login
LOGIN_REDIRECT_URL = 'http://localhost:3000/auth/callback'
ACCOUNT_LOGOUT_REDIRECT_URL = 'http://localhost:3000'
# Allauth settings
ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_AUTHENTICATION_METHOD = 'username'
ACCOUNT_EMAIL_REQUIRED = False
SOCIALACCOUNT_AUTO_SIGNUP = True
SOCIALACCOUNT_STORE_TOKENS = True # CRITICAL: Store OAuth tokens
# GitHub provider specific settings
SOCIALACCOUNT_PROVIDERS = {
'github': {
'SCOPE': [
'user',
'repo',
'read:org',
],
}
}
3. GitHub Client Implementation
# github_integration/client.py
from github import Github as GithubAPI, GithubException
from cryptography.fernet import Fernet
import os
class GitHubClient:
"""GitHub API client for managing repositories and commits"""
def __init__(self, access_token=None):
"""
Initialize GitHub client with access token
Args:
access_token: GitHub personal access token or OAuth token
"""
self.access_token = access_token
self.client = GithubAPI(access_token) if access_token else None
def get_user(self):
"""Get authenticated user information"""
if not self.client:
raise ValueError("GitHub client not authenticated")
return self.client.get_user()
def create_repository(self, name, description="", private=True, auto_init=True):
"""
Create a new GitHub repository
Args:
name: Repository name
description: Repository description
private: Whether the repository should be private
auto_init: Initialize with README
Returns:
Repository object
"""
try:
user = self.get_user()
repo = user.create_repo(
name=name,
description=description,
private=private,
auto_init=auto_init
)
return {
'id': repo.id,
'name': repo.name,
'full_name': repo.full_name,
'html_url': repo.html_url,
'clone_url': repo.clone_url,
'default_branch': repo.default_branch,
'private': repo.private
}
except GithubException as e:
raise Exception(f"Failed to create repository: {e.data.get('message', str(e))}")
def get_repository(self, repo_name):
"""Get repository by name (format: "owner/repo")"""
try:
return self.client.get_repo(repo_name)
except GithubException as e:
raise Exception(f"Failed to get repository: {e.data.get('message', str(e))}")
def create_or_update_file(self, repo_name, file_path, content, commit_message, branch="main"):
"""
Create or update a file (checks if exists first)
Args:
repo_name: Repository name (format: "owner/repo")
file_path: Path to file in repository
content: File content
commit_message: Commit message
branch: Branch name
Returns:
Commit information
"""
try:
repo = self.get_repository(repo_name)
try:
# Try to get the file (will raise exception if doesn't exist)
file = repo.get_contents(file_path, ref=branch)
# File exists, update it
result = repo.update_file(
path=file_path,
message=commit_message,
content=content,
sha=file.sha,
branch=branch
)
except GithubException:
# File doesn't exist, create it
result = repo.create_file(
path=file_path,
message=commit_message,
content=content,
branch=branch
)
return {
'commit': result['commit'].sha,
'content': result['content'].path
}
except Exception as e:
raise Exception(f"Failed to create/update file: {str(e)}")
def list_repositories(self):
"""List all repositories for authenticated user"""
try:
user = self.get_user()
repos = user.get_repos()
return [{
'id': repo.id,
'name': repo.name,
'full_name': repo.full_name,
'html_url': repo.html_url,
'private': repo.private,
'description': repo.description
} for repo in repos]
except GithubException as e:
raise Exception(f"Failed to list repositories: {e.data.get('message', str(e))}")
class TokenEncryption:
"""Utility class for encrypting/decrypting GitHub tokens"""
@staticmethod
def get_encryption_key():
"""Get or generate encryption key"""
key = os.environ.get('GITHUB_TOKEN_ENCRYPTION_KEY')
if not key:
key = Fernet.generate_key().decode()
print(f"Add to .env: GITHUB_TOKEN_ENCRYPTION_KEY={key}")
return key.encode() if isinstance(key, str) else key
@staticmethod
def encrypt_token(token):
"""Encrypt GitHub access token"""
key = TokenEncryption.get_encryption_key()
f = Fernet(key)
return f.encrypt(token.encode()).decode()
@staticmethod
def decrypt_token(encrypted_token):
"""Decrypt GitHub access token"""
key = TokenEncryption.get_encryption_key()
f = Fernet(key)
return f.decrypt(encrypted_token.encode()).decode()
4. Models
# github_integration/models.py
from django.db import models
from django.contrib.auth.models import User
from projects.models import Project
from .client import TokenEncryption
class GitHubAccount(models.Model):
"""Store GitHub account information for users"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='github_account')
github_id = models.BigIntegerField(unique=True) # Use BigIntegerField for GitHub IDs
username = models.CharField(max_length=255)
email = models.EmailField(blank=True, null=True) # Allow null for privacy
avatar_url = models.URLField(blank=True)
access_token_encrypted = models.TextField() # Store encrypted token
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'github_accounts'
def __str__(self):
return f"{self.username} (GitHub)"
def set_access_token(self, token):
"""Encrypt and store access token"""
self.access_token_encrypted = TokenEncryption.encrypt_token(token)
def get_access_token(self):
"""Decrypt and return access token"""
return TokenEncryption.decrypt_token(self.access_token_encrypted)
class GitHubRepository(models.Model):
"""Store GitHub repository information linked to projects"""
project = models.OneToOneField(Project, on_delete=models.CASCADE, related_name='github_repository')
github_account = models.ForeignKey(GitHubAccount, on_delete=models.CASCADE, related_name='repositories')
repo_id = models.BigIntegerField() # Use BigIntegerField for GitHub repo IDs
name = models.CharField(max_length=255)
full_name = models.CharField(max_length=255) # owner/repo format
html_url = models.URLField()
clone_url = models.URLField()
default_branch = models.CharField(max_length=100, default='main')
is_private = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'github_repositories'
unique_together = ['github_account', 'repo_id']
def __str__(self):
return self.full_name
class GitHubCommit(models.Model):
"""Track commits made to GitHub repositories"""
repository = models.ForeignKey(GitHubRepository, on_delete=models.CASCADE, related_name='commits')
sha = models.CharField(max_length=40)
message = models.TextField()
author = models.CharField(max_length=255)
branch = models.CharField(max_length=100, default='main')
file_path = models.CharField(max_length=500, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'github_commits'
ordering = ['-created_at']
def __str__(self):
return f"{self.sha[:7]} - {self.message[:50]}"
5. Signals for Auto-Token Storage
# github_integration/signals.py
from django.dispatch import receiver
from allauth.socialaccount.signals import social_account_added
from .models import GitHubAccount
@receiver(social_account_added)
def save_github_account(sender, request, sociallogin, **kwargs):
"""Automatically save GitHub account info when user connects"""
if sociallogin.account.provider == 'github':
user = sociallogin.user
extra_data = sociallogin.account.extra_data
github_account, created = GitHubAccount.objects.get_or_create(
user=user,
defaults={
'github_id': extra_data.get('id'),
'username': extra_data.get('login'),
'email': extra_data.get('email', ''),
'avatar_url': extra_data.get('avatar_url', ''),
}
)
# Store encrypted access token
token = sociallogin.token.token
github_account.set_access_token(token)
github_account.save()
6. Apps Configuration
# github_integration/apps.py
from django.apps import AppConfig
class GithubIntegrationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'github_integration'
def ready(self):
import github_integration.signals # Import signals
7. Serializers
# github_integration/serializers.py
from rest_framework import serializers
from .models import GitHubAccount, GitHubRepository, GitHubCommit
class GitHubAccountSerializer(serializers.ModelSerializer):
class Meta:
model = GitHubAccount
fields = ['id', 'github_id', 'username', 'email', 'avatar_url', 'created_at']
read_only_fields = ['id', 'github_id', 'created_at']
class GitHubRepositorySerializer(serializers.ModelSerializer):
github_account_username = serializers.CharField(source='github_account.username', read_only=True)
project_name = serializers.CharField(source='project.name', read_only=True)
class Meta:
model = GitHubRepository
fields = [
'id', 'project', 'github_account', 'github_account_username',
'project_name', 'repo_id', 'name', 'full_name', 'html_url',
'clone_url', 'default_branch', 'is_private', 'created_at'
]
read_only_fields = ['id', 'repo_id', 'created_at']
class GitHubCommitSerializer(serializers.ModelSerializer):
repository_name = serializers.CharField(source='repository.full_name', read_only=True)
sha_short = serializers.SerializerMethodField()
class Meta:
model = GitHubCommit
fields = [
'id', 'repository', 'repository_name', 'sha', 'sha_short',
'message', 'author', 'branch', 'file_path', 'created_at'
]
read_only_fields = ['id', 'created_at']
def get_sha_short(self, obj):
return obj.sha[:7]
class RepositoryCreateSerializer(serializers.Serializer):
"""Serializer for creating a new GitHub repository"""
project_id = serializers.IntegerField()
name = serializers.CharField(max_length=255)
description = serializers.CharField(required=False, allow_blank=True)
private = serializers.BooleanField(default=True)
auto_init = serializers.BooleanField(default=True)
class CommitFileSerializer(serializers.Serializer):
"""Serializer for committing a file to GitHub"""
file_path = serializers.CharField(max_length=500)
content = serializers.CharField()
commit_message = serializers.CharField()
branch = serializers.CharField(default='main')
8. API Views
# github_integration/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.shortcuts import get_object_or_404
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
from .models import GitHubAccount, GitHubRepository, GitHubCommit
from .serializers import (
GitHubAccountSerializer,
GitHubRepositorySerializer,
GitHubCommitSerializer,
RepositoryCreateSerializer,
CommitFileSerializer
)
from .client import GitHubClient
from projects.models import Project
class GitHubAccountViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for GitHub accounts"""
serializer_class = GitHubAccountSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return GitHubAccount.objects.filter(user=self.request.user)
class GitHubRepositoryViewSet(viewsets.ModelViewSet):
"""ViewSet for GitHub repositories"""
serializer_class = GitHubRepositorySerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
return GitHubRepository.objects.filter(github_account__user=user)
@action(detail=False, methods=['post'], url_path='create-repository')
def create_repository(self, request):
"""Create a new GitHub repository for a project"""
serializer = RepositoryCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Get project and GitHub account
project = get_object_or_404(Project, id=serializer.validated_data['project_id'])
github_account = get_object_or_404(GitHubAccount, user=request.user)
# Check if repository already exists for this project
if GitHubRepository.objects.filter(project=project).exists():
return Response(
{'error': 'Repository already exists for this project'},
status=status.HTTP_400_BAD_REQUEST
)
try:
# Create repository using GitHub API
client = GitHubClient(github_account.get_access_token())
repo_data = client.create_repository(
name=serializer.validated_data['name'],
description=serializer.validated_data.get('description', ''),
private=serializer.validated_data.get('private', True),
auto_init=serializer.validated_data.get('auto_init', True)
)
# Save repository information
repository = GitHubRepository.objects.create(
project=project,
github_account=github_account,
repo_id=repo_data['id'],
name=repo_data['name'],
full_name=repo_data['full_name'],
html_url=repo_data['html_url'],
clone_url=repo_data['clone_url'],
default_branch=repo_data['default_branch'],
is_private=repo_data['private']
)
return Response(
GitHubRepositorySerializer(repository).data,
status=status.HTTP_201_CREATED
)
except Exception as e:
import traceback
error_msg = str(e) if str(e) else repr(e)
error_trace = traceback.format_exc()
logger.error(f"Error creating GitHub repository: {error_msg}\n{error_trace}")
return Response(
{'error': error_msg, 'detail': error_trace if settings.DEBUG else 'Internal server error'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@action(detail=True, methods=['post'])
def commit_file(self, request, pk=None):
"""Commit a file to the repository"""
repository = self.get_object()
serializer = CommitFileSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
# Get GitHub client
github_account = repository.github_account
client = GitHubClient(github_account.get_access_token())
# Commit file
commit_data = client.create_or_update_file(
repo_name=repository.full_name,
file_path=serializer.validated_data['file_path'],
content=serializer.validated_data['content'],
commit_message=serializer.validated_data['commit_message'],
branch=serializer.validated_data.get('branch', 'main')
)
# Save commit information
commit = GitHubCommit.objects.create(
repository=repository,
sha=commit_data['commit'],
message=serializer.validated_data['commit_message'],
author=github_account.username,
branch=serializer.validated_data.get('branch', 'main'),
file_path=serializer.validated_data['file_path']
)
return Response(
GitHubCommitSerializer(commit).data,
status=status.HTTP_201_CREATED
)
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@action(detail=True, methods=['get'])
def commits(self, request, pk=None):
"""Get all commits for a repository"""
repository = self.get_object()
commits = GitHubCommit.objects.filter(repository=repository)
serializer = GitHubCommitSerializer(commits, many=True)
return Response(serializer.data)
class GitHubCommitViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for GitHub commits"""
serializer_class = GitHubCommitSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
return GitHubCommit.objects.filter(
repository__github_account__user=user
)
9. URL Configuration
# github_integration/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import GitHubAccountViewSet, GitHubRepositoryViewSet, GitHubCommitViewSet
router = DefaultRouter()
router.register(r'accounts', GitHubAccountViewSet, basename='github-account')
router.register(r'repositories', GitHubRepositoryViewSet, basename='github-repository')
router.register(r'commits', GitHubCommitViewSet, basename='github-commit')
urlpatterns = [
path('', include(router.urls)),
]
# config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('allauth.urls')), # GitHub OAuth
path('api/github/', include('github_integration.urls')),
# ... other urls
]
Frontend Implementation
1. API Client
// lib/api.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string = API_BASE_URL) {
this.baseUrl = baseUrl;
}
private getCSRFToken(): string | null {
// Get CSRF token from cookie
const name = 'csrftoken';
let cookieValue: string | null = null;
if (typeof document !== 'undefined' && document.cookie) {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
};
// Add CSRF token for non-GET requests
const csrfToken = this.getCSRFToken();
if (csrfToken && options.method && options.method !== 'GET') {
headers['X-CSRFToken'] = csrfToken;
}
const config: RequestInit = {
...options,
credentials: 'include', // Include cookies for session authentication
headers,
};
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const error: any = new Error(errorData.message || `HTTP error! status: ${response.status}`);
error.response = { status: response.status, data: errorData };
throw error;
}
return await response.json();
}
// GitHub endpoints
async getGitHubAccounts(): Promise<any[]> {
return this.request<any[]>('/github/accounts/');
}
async getGitHubRepositories(): Promise<any[]> {
return this.request<any[]>('/github/repositories/');
}
async createGitHubRepository(data: {
project_id: number;
name: string;
description?: string;
private?: boolean;
auto_init?: boolean;
}): Promise<any> {
return this.request<any>('/github/repositories/create-repository/', {
method: 'POST',
body: JSON.stringify(data),
});
}
async commitFileToGitHub(repositoryId: string, data: {
file_path: string;
content: string;
commit_message: string;
branch?: string;
}): Promise<any> {
return this.request<any>(`/github/repositories/${repositoryId}/commit_file/`, {
method: 'POST',
body: JSON.stringify(data),
});
}
async getRepositoryCommits(repositoryId: string): Promise<any[]> {
return this.request<any[]>(`/github/repositories/${repositoryId}/commits/`);
}
}
// Export singleton instance
export const api = new ApiClient();
2. GitHub Connect Modal Component
// components/github/GitHubConnectModal.tsx
'use client';
import { useState } from 'react';
import { api } from '@/lib/api';
import { GitHubRepository } from '@/lib/types';
interface GitHubConnectModalProps {
isOpen: boolean;
onClose: () => void;
projectId: string;
onSuccess: (repository: GitHubRepository) => void;
}
export default function GitHubConnectModal({
isOpen,
onClose,
projectId,
onSuccess,
}: GitHubConnectModalProps) {
const [step, setStep] = useState<'connect' | 'create'>('connect');
const [repoName, setRepoName] = useState('');
const [description, setDescription] = useState('');
const [isPrivate, setIsPrivate] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
if (!isOpen) return null;
const handleCreateRepository = async () => {
if (!repoName.trim()) {
setError('Repository name is required');
return;
}
setLoading(true);
setError('');
try {
const repository = await api.createGitHubRepository({
project_id: parseInt(projectId),
name: repoName,
description: description || undefined,
private: isPrivate,
auto_init: true,
});
onSuccess(repository);
onClose();
} catch (err: any) {
setError(err.message || 'Failed to create repository');
} finally {
setLoading(false);
}
};
const handleGitHubConnect = () => {
// For now, show the create repository form
// OAuth flow is handled by django-allauth
setStep('create');
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full pixel-border">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900 pixel-font">
๐ Connect GitHub
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-2xl leading-none"
>
ร
</button>
</div>
</div>
{/* Content */}
<div className="px-6 py-4">
{step === 'connect' ? (
<div className="space-y-4">
<p className="text-gray-600">
Connect your GitHub account to automatically create and manage repositories for your project.
</p>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="font-semibold text-blue-900 mb-2">โจ Features</h3>
<ul className="text-sm text-blue-800 space-y-1">
<li>โข Auto-create project repository</li>
<li>โข Commit code changes automatically</li>
<li>โข Track development progress</li>
<li>โข Manage branches and PRs</li>
</ul>
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<button
onClick={handleGitHubConnect}
disabled={loading}
className="w-full bg-gray-900 text-white py-3 rounded-lg hover:bg-gray-800
transition-colors font-semibold flex items-center justify-center gap-2
disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
</svg>
{loading ? 'Connecting...' : 'Connect with GitHub'}
</button>
</div>
) : (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Repository Name *
</label>
<input
type="text"
value={repoName}
onChange={(e) => setRepoName(e.target.value)}
placeholder="my-awesome-project"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2
focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Description
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="A brief description of your project..."
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2
focus:ring-blue-500 focus:border-transparent resize-none"
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="private"
checked={isPrivate}
onChange={(e) => setIsPrivate(e.target.checked)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="private" className="text-sm text-gray-700">
Make repository private
</label>
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<div className="flex gap-3">
<button
onClick={() => setStep('connect')}
disabled={loading}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50
transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Back
</button>
<button
onClick={handleCreateRepository}
disabled={loading || !repoName.trim()}
className="flex-1 bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700
transition-colors font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Creating...' : 'Create Repository'}
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
}
3. OAuth Callback Handler
// app/auth/callback/page.tsx
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
export default function AuthCallback() {
const router = useRouter();
useEffect(() => {
// OAuth callback is handled by django-allauth
// After successful authentication, redirect to dashboard
const timer = setTimeout(() => {
router.push('/');
}, 1000);
return () => clearTimeout(timer);
}, [router]);
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Authenticating...</h1>
<p className="text-gray-600">Please wait while we connect your GitHub account.</p>
</div>
</div>
);
}
Setup Instructions
1. GitHub OAuth App Setup
- Go to https://github.com/settings/developers
- Click "New OAuth App"
- Fill in:
- Application name: Your App Name
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:8000/accounts/github/login/callback/
- Copy Client ID and Client Secret
2. Django Admin Configuration
# Create superuser
python manage.py createsuperuser
# Run migrations
python manage.py makemigrations
python manage.py migrate
# Access admin at http://localhost:8000/admin/
In Django Admin:
- Go to Sites โ Edit site (ID=1) โ Set domain to
localhost:8000(NO http://) - Go to Social applications โ Add social application:
- Provider: GitHub
- Name: GitHub OAuth
- Client id: [Your Client ID]
- Secret key: [Your Client Secret]
- Sites: Select
localhost:8000and move to "Chosen sites"
3. Environment Variables
# backend/.env
GITHUB_TOKEN_ENCRYPTION_KEY=<generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())">
SECRET_KEY=your-django-secret-key
DEBUG=True
# frontend/.env.local
NEXT_PUBLIC_API_URL=http://localhost:8000/api
Common Issues & Solutions
Issue: "SocialApp.DoesNotExist"
Solution: Ensure Site domain is localhost:8000 (NO http://) and Social App is linked to correct site.
# Quick fix command
python manage.py shell -c "from django.contrib.sites.models import Site; site = Site.objects.get(id=1); site.domain = 'localhost:8000'; site.save(); print('Fixed!')"
Issue: "Redirect URI mismatch"
Solution: Verify GitHub OAuth App callback URL is exactly: http://localhost:8000/accounts/github/login/callback/
Issue: Token encryption errors
Solution: Generate and set GITHUB_TOKEN_ENCRYPTION_KEY in .env
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Production Deployment
- Create production GitHub OAuth App with production URLs
- Update Django settings:
- Set
DEBUG = False - Update
ALLOWED_HOSTS - Set
SESSION_COOKIE_SECURE = True - Set
CSRF_COOKIE_SECURE = True
- Set
- Update Site domain in Django Admin to production domain (e.g.,
yourdomain.com) - Use HTTPS for all URLs
Key Features Implemented
โ
GitHub OAuth authentication with django-allauth
โ
Automatic token encryption and storage via signals
โ
Repository creation via PyGithub API
โ
File commit functionality
โ
Commit history tracking
โ
Frontend modal for repository creation
โ
Secure token management with Fernet encryption
โ
Error handling and validation
โ
CSRF protection for API requests
Critical Implementation Details
- Use
BigIntegerFieldfor GitHub IDs - GitHub IDs can exceed 32-bit integer limits - Field name is
access_token_encrypted- Notencrypted_access_token - Email field allows null - Some users may not have public email
- Signal must be imported in apps.py - Add
def ready()method - SOCIALACCOUNT_STORE_TOKENS = True - Required to store OAuth tokens
- Site domain has NO protocol - Use
localhost:8000, nothttp://localhost:8000 - CSRF token required for POST requests - Frontend must include X-CSRFToken header
- Credentials: 'include' - Required for session cookies in fetch requests
Testing
# Backend tests
python manage.py test github_integration
# Test OAuth flow
1. Visit http://localhost:3000
2. Click "Login with GitHub"
3. Authorize application
4. Verify redirect to callback page
5. Check Django Admin for GitHubAccount entry
# Test repository creation
1. Create a project
2. Click "Connect GitHub"
3. Enter repository details
4. Verify repository created on GitHub
5. Check GitHubRepository model in admin
References
Made with โค๏ธ by Bob