From 422ff5f0c7b55d7b1b397225871034b546981c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=86=E6=B2=89?= Date: Sun, 14 Apr 2024 17:16:46 +0800 Subject: [PATCH] app register callback --- server/dao/BaseDAO.py | 7 +++ server/dao/authorization.py | 41 ++++++++++++++++ server/db/supabase/client.py | 9 ++++ server/main.py | 6 ++- server/models/authorization.py | 25 ++++++++++ server/rag/retrieval.py | 6 +-- server/requirements.txt | 4 +- server/routers/github.py | 90 ++++++++++++++++++++++++++++------ 8 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 server/dao/BaseDAO.py create mode 100644 server/dao/authorization.py create mode 100644 server/db/supabase/client.py create mode 100644 server/models/authorization.py diff --git a/server/dao/BaseDAO.py b/server/dao/BaseDAO.py new file mode 100644 index 00000000..40d6ccc3 --- /dev/null +++ b/server/dao/BaseDAO.py @@ -0,0 +1,7 @@ + +from abc import abstractmethod + +class BaseDAO: + @abstractmethod + def get_client(): + ... \ No newline at end of file diff --git a/server/dao/authorization.py b/server/dao/authorization.py new file mode 100644 index 00000000..eabbd550 --- /dev/null +++ b/server/dao/authorization.py @@ -0,0 +1,41 @@ + +import json +from dao.BaseDAO import BaseDAO +from db.supabase.client import get_client +from models.authorization import Authorization +from supabase.client import Client, create_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/db/supabase/client.py b/server/db/supabase/client.py new file mode 100644 index 00000000..ceb53cab --- /dev/null +++ b/server/db/supabase/client.py @@ -0,0 +1,9 @@ +from supabase.client import Client, create_client +from uilts.env import get_env_variable + +supabase_url = get_env_variable("SUPABASE_URL") +supabase_key = get_env_variable("SUPABASE_SERVICE_KEY") + +def get_client(): + supabase: Client = create_client(supabase_url, supabase_key) + return supabase diff --git a/server/main.py b/server/main.py index c8df4901..2463ead0 100644 --- a/server/main.py +++ b/server/main.py @@ -14,6 +14,7 @@ from routers import health_checker, messages, github open_api_key = get_env_variable("OPENAI_API_KEY") +is_dev = bool(get_env_variable("IS_DEV")) app = FastAPI( title="Bo-meta Server", @@ -51,4 +52,7 @@ def search_knowledge(query: str): return data if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8080"))) + if is_dev: + uvicorn.run("main:app", host="0.0.0.0", port=int(os.environ.get("PORT", "8080")), reload=True) + else: + uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8080"))) \ No newline at end of file diff --git a/server/models/authorization.py b/server/models/authorization.py new file mode 100644 index 00000000..70acd2e0 --- /dev/null +++ b/server/models/authorization.py @@ -0,0 +1,25 @@ +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/rag/retrieval.py b/server/rag/retrieval.py index 86e2c190..2c48f163 100644 --- a/server/rag/retrieval.py +++ b/server/rag/retrieval.py @@ -4,7 +4,7 @@ from langchain_openai import OpenAIEmbeddings from langchain_text_splitters import CharacterTextSplitter from langchain_community.vectorstores import SupabaseVectorStore -from supabase.client import Client, create_client +from db.supabase.client import get_client from uilts.env import get_env_variable supabase_url = get_env_variable("SUPABASE_URL") @@ -13,8 +13,6 @@ query_name="match_antd_knowledge" chunk_size=500 -supabase: Client = create_client(supabase_url, supabase_key) - def convert_document_to_dict(document): return { 'page_content': document.page_content, @@ -26,7 +24,7 @@ def init_retriever(): embeddings = OpenAIEmbeddings() db = SupabaseVectorStore( embedding=embeddings, - client=supabase, + client=get_client(), table_name=table_name, query_name=query_name, chunk_size=chunk_size, diff --git a/server/requirements.txt b/server/requirements.txt index 2a781d52..3819c78e 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -13,4 +13,6 @@ python-multipart httpx[socks] load_dotenv supabase -boto3>=1.26.79 +boto3>=1.34.84 +jwt +pydantic>=2.7.0 \ No newline at end of file diff --git a/server/routers/github.py b/server/routers/github.py index 5e4fae12..a6a113dd 100644 --- a/server/routers/github.py +++ b/server/routers/github.py @@ -1,11 +1,16 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, BackgroundTasks, Header, Request import logging import requests -from pydantic import BaseModel - +import time +from dao.authorization import AuthorizationDAO +import boto3 +from botocore.exceptions import ClientError +from jwt import JWT, jwk_from_pem +from models.authorization import Authorization from uilts.env import get_env_variable +APP_ID = get_env_variable("GITHUB_APP_ID") CLIENT_ID = get_env_variable("GITHUB_APP_CLIENT_ID") CLIENT_SECRET = get_env_variable("GITHUB_APP_CLIENT_SECRET") @@ -17,22 +22,79 @@ responses={404: {"description": "Not found"}}, ) +def get_pem(): + secret_name = "prod/githubapp/petercat/pem" + region_name = "ap-northeast-1" + session = boto3.session.Session() + client = session.client( + service_name='secretsmanager', + region_name=region_name + ) + try: + get_secret_value_response = client.get_secret_value( + SecretId=secret_name + ) + except ClientError as e: + # For a list of exceptions thrown, see + # https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html + raise e + + return get_secret_value_response['SecretString'] + +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_pem() + signing_key = jwk_from_pem(pem.encode("utf-8")) -@router.get("/app/callback") -def github_app_callback(code: str): - print("Github App Callback", code) - logger.info("Github App Callback: %s", code) - resp = requests.post('https://github.com/login/oauth/access_token', - json={ - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, - "code": code, + 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): + 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) + + return { "success": success, "message": message } + @router.post("/app/webhook") -def github_app_webhook(callbackParams): - logger.info("Github App Webhook: %s", callbackParams) +def github_app_webhook(request: Request, background_tasks: BackgroundTasks, x_github_event: str = Header(...)): + logger.info("github_app_webhook: x_github_event=%s, %s", x_github_event, request.json()) return {"hello": "world"}