Files
2025-11-05 09:35:50 +00:00

138 lines
4.5 KiB
Python

from typing import Optional
import requests
from django.conf import settings
from django.contrib.auth import login
from django.contrib.auth.models import User
from django.db import models, transaction
from django.utils.timezone import now
from github import Github
from requests import RequestException
from rest_framework import status
from rest_framework.exceptions import APIException
from ..middleware import Request
from .profile import Profile
from .scratch import Scratch
class BadOAuthCodeException(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
default_code = "bad_oauth_code"
default_detail = "Invalid or expired GitHub OAuth verification code."
class MissingOAuthScopeException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_code = "missing_oauth_scope"
class MalformedGitHubApiResponseException(APIException):
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_code = "malformed_github_api_response"
default_detail = "The GitHub API returned an malformed or unexpected response."
class GitHubUser(models.Model):
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
primary_key=True,
related_name="github",
)
github_id = models.PositiveIntegerField(unique=True, editable=False)
class Meta:
verbose_name = "GitHub user"
verbose_name_plural = "GitHub users"
def __str__(self) -> str:
return "@" + self.user.username
@staticmethod
@transaction.atomic
def login(request: Request, oauth_code: str) -> "GitHubUser":
try:
response = requests.post(
"https://github.com/login/oauth/access_token",
json={
"client_id": settings.GITHUB_CLIENT_ID,
"client_secret": settings.GITHUB_CLIENT_SECRET,
"code": oauth_code,
},
headers={"Accept": "application/json"},
timeout=5,
)
response_json = response.json()
except RequestException as e:
raise MalformedGitHubApiResponseException(
f"GitHub API login request failed: {e}."
)
except ValueError:
raise MalformedGitHubApiResponseException(
"GitHub API returned invalid JSON."
)
error: Optional[str] = response_json.get("error")
if error == "bad_verification_code":
raise BadOAuthCodeException()
elif error:
raise MalformedGitHubApiResponseException(
f"GitHub API login sent unknown error '{error}'."
)
try:
access_token = str(response_json["access_token"])
except KeyError:
raise MalformedGitHubApiResponseException()
details = Github(access_token).get_user()
try:
gh_user = GitHubUser.objects.get(github_id=details.id)
except GitHubUser.DoesNotExist:
gh_user = GitHubUser()
user = request.user
# make a new user if request.user already has a github account attached
if (
user.is_anonymous
or isinstance(user, User)
and GitHubUser.objects.filter(user=user).exists()
):
user = User.objects.create_user(
username=details.login,
email=details.email,
password=None,
)
assert isinstance(user, User)
gh_user.user = user
gh_user.github_id = details.id
# If the Github username has changed, update the site's username to match it
if gh_user.user.username != details.login:
gh_user.user.username = details.login
gh_user.user.save(update_fields=["username"])
gh_user.save()
profile: Profile = (
Profile.objects.filter(user=gh_user.user).first() or Profile()
)
profile.user = gh_user.user
profile.last_request_date = now()
profile.save()
# If the previous profile was anonymous, give its scratches to the logged-in profile
if request.profile.is_anonymous() and profile.id != request.profile.id:
Scratch.objects.filter(owner=request.profile).update(owner=profile)
request.profile.delete()
login(request, gh_user.user)
request.profile = profile
request.session["profile_id"] = profile.id
return gh_user