diff --git a/client/app/github/installed/page.tsx b/client/app/github/installed/page.tsx new file mode 100644 index 00000000..8f889533 --- /dev/null +++ b/client/app/github/installed/page.tsx @@ -0,0 +1,19 @@ +'use client'; + +import React from 'react'; + +export default function GithubAppInstalled() { + return ( +
+

+ Installation Approved +

+

+ Thank you for installing PeterCat's GitHub App! +

+

+ Your Team will now be able to use robots for your GitHub organization! +

+
+ ) +} diff --git a/server/dao/BaseDAO.py b/server/dao/BaseDAO.py new file mode 100644 index 00000000..7e8f3033 --- /dev/null +++ b/server/dao/BaseDAO.py @@ -0,0 +1,6 @@ +from abc import abstractmethod + +class BaseDAO: + @abstractmethod + def get_client(): + ... \ No newline at end of file diff --git a/server/dao/authorizationDAO.py b/server/dao/authorizationDAO.py new file mode 100644 index 00000000..bcde5d0c --- /dev/null +++ b/server/dao/authorizationDAO.py @@ -0,0 +1,41 @@ + +import json +from dao.BaseDAO import BaseDAO +from models.authorization import Authorization +from supabase.client import Client, create_client + +from petercat_utils.db.client.supabase import get_client + +class AuthorizationDAO(BaseDAO): + client: Client + + def __init__(self): + super().__init__() + self.client = get_client() + + def exists(self, installation_id: str) -> bool: + try: + authorization = self.client.table("github_app_authorization")\ + .select('*', count="exact")\ + .eq('installation_id', installation_id) \ + .execute() + + return bool(authorization.count) + + except Exception as e: + print("Error: ", e) + return {"message": "User creation failed"} + + def create(self, data: Authorization): + print('supabase github_app_authorization creation', data.model_dump()) + try: + authorization = self.client.from_("github_app_authorization")\ + .insert(data.model_dump())\ + .execute() + if authorization: + return True, {"message": "User created successfully"} + else: + return False, {"message": "User creation failed"} + except Exception as e: + print("Error: ", e) + return {"message": "User creation failed"} \ No newline at end of file diff --git a/server/models/authorization.py b/server/models/authorization.py new file mode 100644 index 00000000..c75423bd --- /dev/null +++ b/server/models/authorization.py @@ -0,0 +1,26 @@ +from datetime import datetime +import json +from pydantic import BaseModel, field_serializer +from typing import Any, Dict + +class Authorization(BaseModel): + token: str + installation_id: str + code: str + created_at: datetime + expires_at: datetime + + permissions: Dict + + @field_serializer('created_at') + def serialize_created_at(self, created_at: datetime): + return created_at.isoformat() + + @field_serializer('expires_at') + def serialize_expires_at(self, expires_at: datetime): + return expires_at.isoformat() + + @field_serializer('permissions') + def serialize_permissions(self, permissions: Dict): + return json.dumps(permissions) + \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt index 86adde2b..f3d7c062 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -14,7 +14,7 @@ httpx[socks] load_dotenv supabase boto3>=1.34.84 -pyjwt>=2.4.0 +jwt pydantic>=2.7.0 unstructured[md] python-dotenv diff --git a/server/routers/github.py b/server/routers/github.py index 9b378e8f..12674337 100644 --- a/server/routers/github.py +++ b/server/routers/github.py @@ -1,10 +1,17 @@ from fastapi import APIRouter, BackgroundTasks, Header, Request import logging +from fastapi.responses import RedirectResponse +import requests +import time from github import Auth +from dao.authorizationDAO import AuthorizationDAO +from models.authorization import Authorization from utils.github import get_handler, get_private_key from petercat_utils import get_env_variable +from jwt import JWT, jwk_from_pem APP_ID = get_env_variable("X_GITHUB_APP_ID") +WEB_URL = get_env_variable("WEB_URL") logger = logging.getLogger() logger.setLevel("INFO") @@ -15,12 +22,57 @@ responses={404: {"description": "Not found"}}, ) +def get_jwt(): + payload = { + # Issued at time + 'iat': int(time.time()), + # JWT expiration time (10 minutes maximum) + 'exp': int(time.time()) + 600, + # GitHub App's identifier + 'iss': APP_ID + } + pem = get_private_key() + signing_key = jwk_from_pem(pem.encode("utf-8")) + + print(pem) + jwt_instance = JWT() + return jwt_instance.encode(payload, signing_key, alg='RS256') + +def get_app_installations_access_token(installation_id: str, jwt: str): + url = f"https://api.github.com/app/installations/{installation_id}/access_tokens" + print("get_app_installations_access_token", url, jwt) + resp = requests.post(url, + headers={ + 'X-GitHub-Api-Version': '2022-11-28', + 'Accept': 'application/vnd.github+json', + 'Authorization': f"Bearer {jwt}" + } + ) + + return resp.json() # https://github.com/login/oauth/authorize?client_id=Iv1.c2e88b429e541264 @router.get("/app/installation/callback") def github_app_callback(code: str, installation_id: str, setup_action: str): - return {"success": True} + authorizationDAO = AuthorizationDAO() + if setup_action != "install": + return { "success": False, "message": f"Invalid setup_action value {setup_action}" } + elif authorizationDAO.exists(installation_id=installation_id): + return { "success": False, "message": f"Installation_id {installation_id} Exists" } + else: + jwt = get_jwt() + access_token = get_app_installations_access_token(installation_id=installation_id, jwt=jwt) + authorization = Authorization( + **access_token, + code=code, + installation_id=installation_id, + created_at=int(time.time()) + ) + + success, message = authorizationDAO.create(authorization) + print(f"github_app_callback: success={success}, message={message}") + return RedirectResponse(url=f'{WEB_URL}/github/installed?message={message}', status_code=302) @router.post("/app/webhook") async def github_app_webhook(