diff --git a/.gitignore b/.gitignore index fe856ff9..ab8b73cb 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ yarn-error.log* # local env files .env*.local +.env*.pre .env *.env # vercel diff --git a/client/app/hooks/useUser.ts b/client/app/hooks/useUser.ts new file mode 100644 index 00000000..c19e8c27 --- /dev/null +++ b/client/app/hooks/useUser.ts @@ -0,0 +1,17 @@ +import { + getUserInfo +} from '@/app/services/UserController'; +import { useQuery } from '@tanstack/react-query'; + + + +function useUser() { + const apiDomain = process.env.NEXT_PUBLIC_API_DOMAIN; + return useQuery({ + queryKey: [`user.userinfo`], + queryFn: async () => getUserInfo(), + retry: false, + }); +} + +export default useUser; diff --git a/client/app/services/UserController.ts b/client/app/services/UserController.ts new file mode 100644 index 00000000..17aa8ae2 --- /dev/null +++ b/client/app/services/UserController.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; + + +// Get the public bot profile by id +export async function getUserInfo() { + + const apiDomain = process.env.NEXT_PUBLIC_API_DOMAIN; + const response = await axios.get(`${apiDomain}/api/auth/userinfo`); + return response.data.data; +} \ No newline at end of file diff --git a/client/components/User.tsx b/client/components/User.tsx index bce5e73e..d4ffc51b 100644 --- a/client/components/User.tsx +++ b/client/components/User.tsx @@ -1,12 +1,13 @@ -import { useUser } from '@auth0/nextjs-auth0/client'; import { Avatar, Button, Link } from '@nextui-org/react'; +import useUser from '../app/hooks/useUser'; export default function Profile() { - const { user } = useUser(); + const { data: user } = useUser(); + const apiDomain = process.env.NEXT_PUBLIC_API_DOMAIN; if (!user) { return ( - ); diff --git a/client/middleware.ts b/client/middleware.ts index 028e0def..5b2dd272 100644 --- a/client/middleware.ts +++ b/client/middleware.ts @@ -6,7 +6,7 @@ export async function middleware(req: NextRequest) { const res = NextResponse.next() const session = await getSession(); - + if (!session?.user) { return NextResponse.redirect(new URL('/api/auth/login', req.url)); } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..30b0f008 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +python-dotenv +python-jose +six +jose \ No newline at end of file diff --git a/server/main.py b/server/main.py index 7ac25c8d..295edeca 100644 --- a/server/main.py +++ b/server/main.py @@ -3,18 +3,19 @@ import uvicorn from fastapi import FastAPI from fastapi.responses import StreamingResponse -from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware + from agent import stream from uilts.env import get_env_variable from data_class import ChatData # Import fastapi routers -from routers import bot, health_checker, github, rag +from routers import bot, health_checker, github, rag, auth open_api_key = get_env_variable("OPENAI_API_KEY") is_dev = bool(get_env_variable("IS_DEV")) - +session_secret_key = get_env_variable("FASTAPI_SECRET_KEY") app = FastAPI( title="Bo-meta Server", version="1.0", @@ -22,18 +23,15 @@ ) app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - expose_headers=["*"], + SessionMiddleware, + secret_key = session_secret_key, ) app.include_router(health_checker.router) app.include_router(github.router) app.include_router(rag.router) app.include_router(bot.router) +app.include_router(auth.router) @app.post("/api/chat/stream", response_class=StreamingResponse) def run_agent_chat(input_data: ChatData): diff --git a/server/routers/auth.py b/server/routers/auth.py new file mode 100644 index 00000000..27c13023 --- /dev/null +++ b/server/routers/auth.py @@ -0,0 +1,97 @@ +from fastapi import APIRouter,Cookie, Depends, Security, Request, HTTPException, status, Response +from uilts.env import get_env_variable +from fastapi_auth0 import Auth0, Auth0User +from fastapi.responses import RedirectResponse, JSONResponse +import httpx +from db.supabase.client import get_client +import secrets + +AUTH0_DOMAIN = get_env_variable("AUTH0_DOMAIN") + +API_AUDIENCE = get_env_variable("API_IDENTIFIER") +CLIENT_ID = get_env_variable("AUTH0_CLIENT_ID") +CLIENT_SECRET = get_env_variable("AUTH0_CLIENT_SECRET") + +API_URL = get_env_variable("API_URL") +CALLBACK_URL = f"{API_URL}/api/auth/callback" +LOGIN_URL = f"{API_URL}/api/auth/login" + +WEB_URL = get_env_variable("WEB_URL") + + +auth = Auth0(domain=AUTH0_DOMAIN, api_audience=API_AUDIENCE, scopes={'read': 'get list'}) + +router = APIRouter( + prefix="/api/auth", + tags=["auth"], + responses={404: {"description": "Not found"}}, +) + +async def getUserInfoByToken(token): + userinfo_url = f"https://{AUTH0_DOMAIN}/userinfo" + + + headers = {"authorization": f"Bearer {token}"} + async with httpx.AsyncClient() as client: + user_info_response = await client.get(userinfo_url, headers=headers) + if user_info_response.status_code == 200: + user_info = user_info_response.json() + data = { + "id": user_info["sub"], + "nickname": user_info.get("nickname"), + "name": user_info.get("name"), + "picture": user_info.get("picture"), + "sub": user_info["sub"], + "sid": secrets.token_urlsafe(32) + } + return data + else : + return {} + +async def getTokenByCode(code): + token_url = f"https://{AUTH0_DOMAIN}/oauth/token" + headers = {"content-type": "application/x-www-form-urlencoded"} + data = { + "grant_type": "authorization_code", + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "code": code, + "redirect_uri": CALLBACK_URL, + } + async with httpx.AsyncClient() as client: + response = await client.post(token_url, headers=headers, data=data) + token_response = response.json() + + if "access_token" not in token_response: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Failed to get access token") + return token_response['access_token'] + + +@router.get("/login") +def login(): + redirect_uri = f"https://{AUTH0_DOMAIN}/authorize?audience={API_AUDIENCE}&response_type=code&client_id={CLIENT_ID}&redirect_uri={CALLBACK_URL}&scope=openid profile email&state=STATE" + return RedirectResponse(redirect_uri) + +@router.get("/callback") +async def callback(request: Request, response: Response): + code = request.query_params.get("code") + if not code: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing authorization code") + token = await getTokenByCode(code) + data = await getUserInfoByToken(token) + + supabase = get_client() + supabase.table("profiles").upsert(data).execute() + response = RedirectResponse(url=f'{WEB_URL}', status_code=302) # 303 See Other 确保正确处理 POST 到 GET 的重定向 + response.set_cookie(key="petercat", value=token, httponly=True, secure=True, samesite='Lax') + return response + +@router.get("/userinfo") +async def userinfo(petercat: str = Cookie(None)): + if not petercat: + return RedirectResponse(url=LOGIN_URL, status_code=303) + data = await getUserInfoByToken(petercat) + if data : + return { "data": data, "status": 200} + else: + return RedirectResponse(url=LOGIN_URL, status_code=303) \ No newline at end of file diff --git a/server/routers/bot.py b/server/routers/bot.py index e6246a32..d513a3b4 100644 --- a/server/routers/bot.py +++ b/server/routers/bot.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, Query, Body, Path +from fastapi import APIRouter, Query, Body, Path from db.supabase.client import get_client from type_class.bot import BotUpdateRequest, BotCreateRequest from typing import Optional