Skip to content
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

feat: support local auth #593

Merged
merged 4 commits into from
Dec 17, 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
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']

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure to handle potential exceptions when accessing response.json()['access_token'] to avoid runtime errors in case of unexpected response formats or errors.


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()
RaoHai marked this conversation as resolved.
Show resolved Hide resolved
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
Loading