Skip to content

Commit

Permalink
feat: support local auth (#593)
Browse files Browse the repository at this point in the history
-  支持通过环境变量跳过 auth0 登录。本地模拟登录
  • Loading branch information
RaoHai authored Dec 17, 2024
2 parents 594eaf2 + 719dab2 commit c0876b8
Show file tree
Hide file tree
Showing 20 changed files with 272 additions and 204 deletions.
2 changes: 1 addition & 1 deletion assistant/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@petercatai/assistant",
"version": "1.0.20",
"version": "1.0.22",
"description": "PeterCat Assistant Application",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
Expand Down
4 changes: 2 additions & 2 deletions assistant/src/Chat/template/LoginCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Button } from 'antd';
import GitHubIcon from '../../icons/GitHubIcon';
import useUser from '../../hooks/useUser';

const LoginCard = ({ apiDomain, token }: { apiDomain: string; token: string; }) => {
const { user, isLoading, actions } = useUser({ apiDomain, fingerprint: token });
const LoginCard = ({ apiDomain, webDomain, token }: { apiDomain: string; webDomain?: string; token: string; }) => {
const { user, isLoading, actions } = useUser({ apiDomain, webDomain, fingerprint: token });

if (isLoading) {
return <Button disabled loading>Loading...</Button>
Expand Down
8 changes: 6 additions & 2 deletions assistant/src/Chat/template/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import GitInsightCard from './GitInsightCard';
import LoginCard from './LoginCard';

export const UITemplateRender = ({ templateId, apiDomain, token, cardData }: { templateId: string, apiDomain: string; token: string; cardData: any }) => {
export const UITemplateRender = ({ templateId, apiDomain, webDomain, token, cardData }: { templateId: string, apiDomain: string; webDomain?: string; token: string; cardData: any }) => {
if (templateId === 'GIT_INSIGHT') {
return (
<GitInsightCard
Expand All @@ -15,7 +15,11 @@ export const UITemplateRender = ({ templateId, apiDomain, token, cardData }: { t

if (templateId === 'LOGIN_INVITE') {
return (
<LoginCard apiDomain={apiDomain} token={token} />
<LoginCard
apiDomain={apiDomain}
webDomain={webDomain}
token={token}
/>
);
}
return null;
Expand Down
5 changes: 3 additions & 2 deletions assistant/src/hooks/useUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import useSWR from 'swr';
import { popupCenter } from '../utils/popcenter';
import { useEffect } from 'react';

function useUser({ apiDomain, fingerprint }: { apiDomain: string; fingerprint: string }) {
function useUser({ apiDomain, webDomain = 'https://petercat.ai', fingerprint }: { apiDomain: string; fingerprint: string; webDomain?: string }) {
const { data: user, isLoading, mutate } = useSWR(
['user.info'],
async () => getUserInfo(apiDomain, { clientId: fingerprint }),
Expand All @@ -14,8 +14,9 @@ function useUser({ apiDomain, fingerprint }: { apiDomain: string; fingerprint: s


const doLogin = () => {
console.log('call do Login', webDomain);
popupCenter({
url: 'https://petercat.ai/user/login',
url: `${webDomain}/user/login`,
title: 'Login',
w: 600,
h: 400,
Expand Down
24 changes: 12 additions & 12 deletions assistant/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@
*/

*:where(.petercat-lui,.petercat-lui *),
:where(.petercat-lui,.petercat-lui *)::before,
:where(.petercat-lui,.petercat-lui *)::after {
::before:where(.petercat-lui,.petercat-lui *),
::after:where(.petercat-lui,.petercat-lui *) {
box-sizing: border-box;
/* 1 */
border-width: 0;
Expand All @@ -124,8 +124,8 @@
/* 2 */
}

:where(.petercat-lui,.petercat-lui *)::before,
:where(.petercat-lui,.petercat-lui *)::after {
::before:where(.petercat-lui,.petercat-lui *),
::after:where(.petercat-lui,.petercat-lui *) {
--tw-content: '';
}

Expand Down Expand Up @@ -378,8 +378,8 @@ progress:where(.petercat-lui,.petercat-lui *) {
Correct the cursor style of increment and decrement buttons in Safari.
*/

:where(.petercat-lui,.petercat-lui *) ::-webkit-inner-spin-button,
:where(.petercat-lui,.petercat-lui *) ::-webkit-outer-spin-button {
::-webkit-inner-spin-button:where(.petercat-lui,.petercat-lui *),
::-webkit-outer-spin-button:where(.petercat-lui,.petercat-lui *) {
height: auto;
}

Expand All @@ -399,7 +399,7 @@ Correct the cursor style of increment and decrement buttons in Safari.
Remove the inner padding in Chrome and Safari on macOS.
*/

:where(.petercat-lui,.petercat-lui *) ::-webkit-search-decoration {
::-webkit-search-decoration:where(.petercat-lui,.petercat-lui *) {
-webkit-appearance: none;
}

Expand All @@ -408,7 +408,7 @@ Remove the inner padding in Chrome and Safari on macOS.
2. Change font properties to `inherit` in Safari.
*/

:where(.petercat-lui,.petercat-lui *) ::-webkit-file-upload-button {
::-webkit-file-upload-button:where(.petercat-lui,.petercat-lui *) {
-webkit-appearance: button;
/* 1 */
font: inherit;
Expand Down Expand Up @@ -481,15 +481,15 @@ textarea:where(.petercat-lui,.petercat-lui *) {
2. Set the default placeholder color to the user's configured gray 400 color.
*/

:where(.petercat-lui,.petercat-lui *) input::-moz-placeholder, :where(.petercat-lui,.petercat-lui *) textarea::-moz-placeholder {
input::-moz-placeholder:where(.petercat-lui,.petercat-lui *), textarea::-moz-placeholder:where(.petercat-lui,.petercat-lui *) {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}

:where(.petercat-lui,.petercat-lui *) input::placeholder,
:where(.petercat-lui,.petercat-lui *) textarea::placeholder {
input::placeholder:where(.petercat-lui,.petercat-lui *),
textarea::placeholder:where(.petercat-lui,.petercat-lui *) {
opacity: 1;
/* 1 */
color: #9ca3af;
Expand Down Expand Up @@ -545,7 +545,7 @@ video:where(.petercat-lui,.petercat-lui *) {

/* Make elements with the HTML hidden attribute stay hidden by default */

[hidden]:where(:not([hidden="until-found"])):where(.petercat-lui,.petercat-lui *) {
[hidden]:where(.petercat-lui,.petercat-lui *) {
display: none;
}

Expand Down
1 change: 1 addition & 0 deletions client/app/hooks/useUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const useUser = () => {
const { data: fingerprint } = useFingerprint();
const { user, isLoading, actions } = useAssistUser({
apiDomain: API_DOMAIN,
webDomain: '',
fingerprint: fingerprint?.visitorId!,
});

Expand Down
1 change: 0 additions & 1 deletion client/components/User.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';
import I18N from '@/app/utils/I18N';
import { useRouter } from 'next/navigation';
import {
Avatar,
Button,
Expand Down
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@fullpage/react-fullpage": "^0.1.42",
"@next/bundle-analyzer": "^13.4.19",
"@nextui-org/react": "^2.2.9",
"@petercatai/assistant": "1.0.20",
"@petercatai/assistant": "1.0.22",
"@sentry/nextjs": "^8.28.0",
"@supabase/supabase-js": "^2.32.0",
"@tanstack/react-query": "^5.17.19",
Expand Down
8 changes: 4 additions & 4 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2842,10 +2842,10 @@
resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.2.1.tgz#cb0d111ef700136f4580349ff0226bf25c853f23"
integrity sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==

"@petercatai/[email protected].20":
version "1.0.20"
resolved "https://registry.yarnpkg.com/@petercatai/assistant/-/assistant-1.0.20.tgz#2d2dc1beb296c8524219a6de7eee1575cb3b4c92"
integrity sha512-csfRRsKB9FbBM+cMcCTQQowsuuFRVerSrxfMRTWoI1XHhBW3ormbt1XTeYKiubmwz4iKznR+2UCrZrCl75ckmA==
"@petercatai/[email protected].22":
version "1.0.22"
resolved "https://registry.yarnpkg.com/@petercatai/assistant/-/assistant-1.0.22.tgz#a4113bf4eae9dc66ad0f0e2b33b1f579ca1252a2"
integrity sha512-E8uMZRK3bdD9Oh2mQhK6Zd2A+KV6dt/H2F/fnv/cBT6KOdywwDQIx94K/2fTcpZJXPsUCTMcOhl2877FNaJkxQ==
dependencies:
"@ant-design/icons" "^5.3.5"
"@ant-design/pro-chat" "^1.9.0"
Expand Down
6 changes: 6 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ X_GITHUB_APP_ID=github_app_id
X_GITHUB_APPS_CLIENT_ID=github_apps_client_id
X_GITHUB_APPS_CLIENT_SECRET=github_apps_client_secret

PETERCAT_AUTH0_ENABLED=False
# OPTIONAL - Local Authorization Configures

PETERCAT_LOCAL_UID="petercat|001"
PETERCAT_LOCAL_UNAME="petercat"

# OPTIONAL - AUTH0 Configures

API_IDENTIFIER=api_identifier
AUTH0_DOMAIN=auth0_domain
AUTH0_CLIENT_ID=auth0_client_id
Expand Down
7 changes: 7 additions & 0 deletions server/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ X_GITHUB_APP_ID=github_app_id
X_GITHUB_APPS_CLIENT_ID=github_apps_client_id
X_GITHUB_APPS_CLIENT_SECRET=github_apps_client_secret

# OPTIONAL - Local Authorization Configures
PETERCAT_LOCAL_UID="petercat|001"
PETERCAT_LOCAL_UNAME="petercat"
PETERCAT_LOCAL_GITHUB_TOKEN="github_pat_xxxx"

# OPTIONAL - SKIP AUTH0 Authorization
PETERCAT_AUTH0_ENABLED=True

# OPTIONAL - AUTH0 Configures
API_IDENTIFIER=api_identifier
Expand Down
12 changes: 12 additions & 0 deletions server/auth/clients/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from auth.clients.auth0 import Auth0Client
from auth.clients.base import BaseAuthClient
from auth.clients.local import LocalClient

from petercat_utils import get_env_variable

PETERCAT_AUTH0_ENABLED = get_env_variable("PETERCAT_AUTH0_ENABLED", "True") == "True"

def get_auth_client() -> BaseAuthClient:
if PETERCAT_AUTH0_ENABLED:
return Auth0Client()
return LocalClient()
92 changes: 92 additions & 0 deletions server/auth/clients/auth0.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import httpx
import secrets

from fastapi import Request
from auth.clients.base import BaseAuthClient
from petercat_utils import get_env_variable
from starlette.config import Config
from authlib.integrations.starlette_client import OAuth

CLIENT_ID = get_env_variable("AUTH0_CLIENT_ID")
CLIENT_SECRET = get_env_variable("AUTH0_CLIENT_SECRET")
AUTH0_DOMAIN = get_env_variable("AUTH0_DOMAIN")
API_AUDIENCE = get_env_variable("API_IDENTIFIER")
API_URL = get_env_variable("API_URL")

CALLBACK_URL = f"{API_URL}/api/auth/callback"

config = Config(
environ={
"AUTH0_CLIENT_ID": CLIENT_ID,
"AUTH0_CLIENT_SECRET": CLIENT_SECRET,
}
)

class Auth0Client(BaseAuthClient):
_client: OAuth

def __init__(self):
self._client = OAuth(config)
self._client.register(
name="auth0",
server_metadata_url=f"https://{AUTH0_DOMAIN}/.well-known/openid-configuration",
client_kwargs={"scope": "openid email profile"},
)

async def login(self, request):
return await self._client.auth0.authorize_redirect(
request, redirect_uri=CALLBACK_URL
)

async def get_oauth_token(self):
url = f'https://{AUTH0_DOMAIN}/oauth/token'
headers = {"content-type": "application/x-www-form-urlencoded"}
data = {
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'audience': API_AUDIENCE,
'grant_type': 'client_credentials'
}
async with httpx.AsyncClient() as client:
response = await client.post(url, data=data, headers=headers)
return response.json()['access_token']

async def get_user_info(self, request: Request) -> dict:
auth0_token = await self._client.auth0.authorize_access_token(request)
access_token = auth0_token["access_token"]
userinfo_url = f"https://{AUTH0_DOMAIN}/userinfo"
headers = {"authorization": f"Bearer {access_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),
"agreement_accepted": user_info.get("agreement_accepted"),
}
return data
else:
return None

async def get_access_token(self, user_id: str, provider="github"):
token = await self.get_oauth_token()
user_accesstoken_url = f"https://{AUTH0_DOMAIN}/api/v2/users/{user_id}"

async with httpx.AsyncClient() as client:
headers = {"authorization": f"Bearer {token}"}
user_info_response = await client.get(user_accesstoken_url, headers=headers)
user = user_info_response.json()
identity = next(
(
identity
for identity in user["identities"]
if identity["provider"] == provider
),
None,
)
return identity["access_token"]
51 changes: 51 additions & 0 deletions server/auth/clients/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import secrets

from abc import abstractmethod
from fastapi import Request
from utils.random_str import random_str
from petercat_utils import get_client


class BaseAuthClient:
def __init__(self):
pass

def generateAnonymousUser(self, clientId: str) -> tuple[str, dict]:
token = f"client|{clientId}"
seed = clientId[:4]
random_name = f"{seed}_{random_str(4)}"
data = {
"id": token,
"sub": token,
"nickname": random_name,
"name": random_name,
"picture": f"https://picsum.photos/seed/{seed}/100/100",
"sid": secrets.token_urlsafe(32),
"agreement_accepted": False,
}

return token, data

async def anonymouseLogin(self, request: Request) -> dict:
clientId = request.query_params.get("clientId") or random_str()
token, data = self.generateAnonymousUser(clientId = clientId)
supabase = get_client()
supabase.table("profiles").upsert(data).execute()
request.session["user"] = data
return data

@abstractmethod
async def login(self, request: Request):
pass

@abstractmethod
async def get_oauth_token(self) -> str:
pass

@abstractmethod
async def get_user_info(self, request: Request) -> dict:
pass

@abstractmethod
async def get_access_token(self, user_id: str, provider="github") -> str:
pass
Loading

0 comments on commit c0876b8

Please sign in to comment.