Skip to content

feat: add auth0 related service #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ yarn-error.log*

# local env files
.env*.local
.env*.pre
.env
*.env
# vercel
Expand Down
17 changes: 17 additions & 0 deletions client/app/hooks/useUser.ts
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions client/app/services/UserController.ts
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 4 additions & 3 deletions client/components/User.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button as={Link} color="primary" href="/api/auth/login" variant="flat">
<Button as={Link} color="primary" href={`${apiDomain}/api/auth/login`} variant="flat">
Login
</Button>
);
Expand Down
2 changes: 1 addition & 1 deletion client/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
python-dotenv
python-jose
six
jose
16 changes: 7 additions & 9 deletions server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,35 @@
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",
description="Agent Chat APIs"
)

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):
Expand Down
97 changes: 97 additions & 0 deletions server/routers/auth.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion server/routers/bot.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading