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(