Commit 910b67cb authored by Hoanganhvu123's avatar Hoanganhvu123

Integrate Clerk auth, memo retrieval tool, and chatbot UI

parent fbd1e348
...@@ -3,23 +3,24 @@ Tools Factory ...@@ -3,23 +3,24 @@ Tools Factory
Chỉ return 1 tool duy nhất: data_retrieval_tool Chỉ return 1 tool duy nhất: data_retrieval_tool
""" """
from langchain_core.tools import Tool from langchain_core.tools import BaseTool
from .brand_knowledge_tool import canifa_knowledge_search from .brand_knowledge_tool import canifa_knowledge_search
from .customer_info_tool import collect_customer_info from .customer_info_tool import collect_customer_info
from .data_retrieval_tool import data_retrieval_tool from .data_retrieval_tool import data_retrieval_tool
from .memo_retrieval_tool import memo_retrieval_tool
def get_retrieval_tools() -> list[Tool]: def get_retrieval_tools() -> list[BaseTool]:
"""Các tool chỉ dùng để đọc/truy vấn dữ liệu (Có thể cache)""" """Các tool chỉ dùng để đọc/truy vấn dữ liệu (Có thể cache)"""
return [data_retrieval_tool, canifa_knowledge_search] return [data_retrieval_tool, canifa_knowledge_search, memo_retrieval_tool]
def get_collection_tools() -> list[Tool]: def get_collection_tools() -> list[BaseTool]:
"""Các tool dùng để ghi/thu thập dữ liệu (KHÔNG cache)""" """Các tool dùng để ghi/thu thập dữ liệu (KHÔNG cache)"""
return [collect_customer_info] return [collect_customer_info]
def get_all_tools() -> list[Tool]: def get_all_tools() -> list[BaseTool]:
"""Return toàn bộ list tools cho Agent""" """Return toàn bộ list tools cho Agent"""
return get_retrieval_tools() + get_collection_tools() return get_retrieval_tools() + get_collection_tools()
"""
Memo Retrieval Tool - đọc ghi chú từ SQLite memos.db cho Agent.
Dùng để trả lời các câu hỏi kiểu:
- "Hôm nay tôi đã note gì?"
- "Tuần trước tôi note những gì về dự án X?"
"""
from __future__ import annotations
import json
import logging
from datetime import date
from typing import Optional
from langchain_core.tools import tool
from memos_core import db as memo_db
logger = logging.getLogger(__name__)
@tool
async def memo_retrieval_tool(
date: str,
tag: Optional[str] = None,
) -> str:
"""
Truy vấn các memo đã note trong memos.db (SQLite).
- Ưu tiên dùng để trả lời câu hỏi về lịch sử note, ví dụ:
- "Hôm nay tôi đã note gì thế?"
- "Ngày 2026-01-15 tôi note những gì?"
- "Hôm nay tôi note gì về tag #work?"
- Không làm semantic search phức tạp, chỉ lọc theo ngày và tag.
"""
try:
target_date = date or date_today_iso()
logger.info("memo_retrieval_tool started, date=%s, tag=%s", target_date, tag)
sql = """
SELECT id, content, visibility, tags_json, created_at, updated_at
FROM memos
WHERE DATE(created_at) = ?
"""
params: list[object] = [target_date]
if tag:
# Lọc đơn giản: tags_json là chuỗi JSON array, tìm substring theo tag.
sql += " AND tags_json LIKE ?"
params.append(f"%{tag}%")
rows = await memo_db.fetch_all(sql, tuple(params))
memos = []
for row in rows:
# Chuẩn hóa tags
raw_tags = row.get("tags_json") or "[]"
try:
tags = json.loads(raw_tags)
if not isinstance(tags, list):
tags = []
except Exception:
tags = []
memos.append(
{
"id": row.get("id"),
"content": row.get("content"),
"visibility": row.get("visibility"),
"tags": tags,
"created_at": row.get("created_at"),
"updated_at": row.get("updated_at"),
}
)
return json.dumps(
{
"status": "success",
"date": target_date,
"tag": tag,
"count": len(memos),
"memos": memos,
},
ensure_ascii=False,
)
except Exception as exc: # pragma: no cover
logger.exception("memo_retrieval_tool error: %s", exc)
return json.dumps(
{
"status": "error",
"message": str(exc),
},
ensure_ascii=False,
)
def date_today_iso() -> str:
"""Return today's date in YYYY-MM-DD format."""
return date.today().isoformat()
from __future__ import annotations
import logging
from functools import lru_cache
from typing import Any
import jwt
from jwt import PyJWKClient
from config import CLERK_ISSUER, CLERK_JWKS_URL
logger = logging.getLogger(__name__)
@lru_cache(maxsize=1)
def _jwks_client() -> PyJWKClient:
if not CLERK_JWKS_URL:
raise ValueError("CLERK_JWKS_URL is not configured")
return PyJWKClient(CLERK_JWKS_URL)
def verify_clerk_jwt(token: str) -> dict[str, Any]:
"""
Verify Clerk JWT using JWKS.
Requires:
- CLERK_JWKS_URL: e.g. https://<your-clerk-domain>/.well-known/jwks.json
- CLERK_ISSUER: e.g. https://<your-clerk-domain>
"""
if not token:
raise ValueError("Missing token")
if not CLERK_JWKS_URL or not CLERK_ISSUER:
raise ValueError("Missing CLERK_JWKS_URL / CLERK_ISSUER")
signing_key = _jwks_client().get_signing_key_from_jwt(token).key
# Clerk tokens are typically RS256.
payload = jwt.decode(
token,
signing_key,
algorithms=["RS256"],
issuer=CLERK_ISSUER,
options={
"verify_aud": False, # allow multiple audiences in dev
},
)
return payload
...@@ -8,10 +8,11 @@ import logging ...@@ -8,10 +8,11 @@ import logging
from collections.abc import Callable from collections.abc import Callable
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from fastapi import HTTPException, Request, status from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from common.clerk_auth import verify_clerk_jwt
from config import DISABLE_AUTH from config import DISABLE_AUTH
from starlette.middleware.base import BaseHTTPMiddleware
if TYPE_CHECKING: if TYPE_CHECKING:
from fastapi import FastAPI from fastapi import FastAPI
...@@ -50,7 +51,7 @@ RATE_LIMITED_PATHS = [ ...@@ -50,7 +51,7 @@ RATE_LIMITED_PATHS = [
class CanifaAuthMiddleware(BaseHTTPMiddleware): class CanifaAuthMiddleware(BaseHTTPMiddleware):
""" """
Canifa Authentication + Rate Limit Middleware Authentication + Rate Limit Middleware
Flow: Flow:
1. Frontend gửi request với Authorization: Bearer <canifa_token> 1. Frontend gửi request với Authorization: Bearer <canifa_token>
...@@ -81,7 +82,7 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware): ...@@ -81,7 +82,7 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
return await call_next(request) return await call_next(request)
# ===================================================================== # =====================================================================
# STEP 1: AUTHENTICATION (Canifa API) # STEP 1: AUTHENTICATION (Clerk JWT)
# ===================================================================== # =====================================================================
try: try:
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
...@@ -104,31 +105,26 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware): ...@@ -104,31 +105,26 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
request.state.is_authenticated = False request.state.is_authenticated = False
request.state.device_id = device_id request.state.device_id = device_id
else: else:
# --- TRƯỜNG HỢP 2: CÓ TOKEN -> GỌI CANIFA VERIFY --- # --- TRƯỜNG HỢP 2: CÓ TOKEN -> VERIFY CLERK JWT ---
token = auth_header.replace("Bearer ", "") token = auth_header.replace("Bearer ", "")
from common.canifa_api import verify_canifa_token, extract_user_id_from_canifa_response
try: try:
user_data = await verify_canifa_token(token) payload = verify_clerk_jwt(token)
user_id = await extract_user_id_from_canifa_response(user_data) user_id = payload.get("sub")
if user_id: if user_id:
request.state.user = user_data request.state.user = payload
request.state.user_id = user_id request.state.user_id = str(user_id)
request.state.token = token request.state.token = token
request.state.is_authenticated = True request.state.is_authenticated = True
request.state.device_id = device_id request.state.device_id = device_id or str(user_id)
logger.debug(f"✅ Canifa Auth Success: User {user_id}") logger.debug("✅ Clerk Auth Success: user_id=%s", user_id)
else: else:
logger.warning(f"⚠️ Invalid Canifa Token -> Guest Mode") logger.warning("⚠️ Clerk token missing sub -> Guest Mode")
request.state.user = None request.state.user = None
request.state.user_id = None request.state.user_id = None
request.state.is_authenticated = False request.state.is_authenticated = False
request.state.device_id = device_id request.state.device_id = device_id
except Exception as e: except Exception as e:
logger.error(f"❌ Canifa Auth Error: {e} -> Guest Mode") logger.error("❌ Clerk Auth Error: %s -> Guest Mode", e)
request.state.user = None request.state.user = None
request.state.user_id = None request.state.user_id = None
request.state.is_authenticated = False request.state.is_authenticated = False
...@@ -169,8 +165,10 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware): ...@@ -169,8 +165,10 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
if not can_send: if not can_send:
logger.warning( logger.warning(
f"⚠️ Rate Limit Exceeded: {rate_limit_key} | " "⚠️ Rate Limit Exceeded: %s | used=%s/%s",
f"used={limit_info['used']}/{limit_info['limit']}" rate_limit_key,
limit_info["used"],
limit_info["limit"],
) )
return JSONResponse( return JSONResponse(
status_code=429, status_code=429,
......
...@@ -18,6 +18,8 @@ __all__ = [ ...@@ -18,6 +18,8 @@ __all__ = [
"CHECKPOINT_POSTGRES_SCHEMA", "CHECKPOINT_POSTGRES_SCHEMA",
"CHECKPOINT_POSTGRES_URL", "CHECKPOINT_POSTGRES_URL",
"CLERK_SECRET_KEY", "CLERK_SECRET_KEY",
"CLERK_JWKS_URL",
"CLERK_ISSUER",
"CONV_DATABASE_URL", "CONV_DATABASE_URL",
"CONV_SUPABASE_KEY", "CONV_SUPABASE_KEY",
"CONV_SUPABASE_URL", "CONV_SUPABASE_URL",
...@@ -78,8 +80,8 @@ OPENAI_API_KEY: str | None = os.getenv("OPENAI_API_KEY") ...@@ -78,8 +80,8 @@ OPENAI_API_KEY: str | None = os.getenv("OPENAI_API_KEY")
GOOGLE_API_KEY: str | None = os.getenv("GOOGLE_API_KEY") GOOGLE_API_KEY: str | None = os.getenv("GOOGLE_API_KEY")
GROQ_API_KEY: str | None = os.getenv("GROQ_API_KEY") GROQ_API_KEY: str | None = os.getenv("GROQ_API_KEY")
DEFAULT_MODEL: str = os.getenv("DEFAULT_MODEL", "gpt-5-nano") DEFAULT_MODEL: str = os.getenv("DEFAULT_MODEL", "gpt-4o-mini")
# DEFAULT_MODEL: str = os.getenv("DEFAULT_MODEL") AI_MODEL_NAME = DEFAULT_MODEL
# ====================== JWT CONFIGURATION ====================== # ====================== JWT CONFIGURATION ======================
JWT_SECRET: str | None = os.getenv("JWT_SECRET") JWT_SECRET: str | None = os.getenv("JWT_SECRET")
...@@ -106,6 +108,8 @@ LANGSMITH_PROJECT = None ...@@ -106,6 +108,8 @@ LANGSMITH_PROJECT = None
# ====================== CLERK AUTHENTICATION ====================== # ====================== CLERK AUTHENTICATION ======================
CLERK_SECRET_KEY: str | None = os.getenv("CLERK_SECRET_KEY") CLERK_SECRET_KEY: str | None = os.getenv("CLERK_SECRET_KEY")
CLERK_JWKS_URL: str | None = os.getenv("CLERK_JWKS_URL")
CLERK_ISSUER: str | None = os.getenv("CLERK_ISSUER")
# ====================== DATABASE CONNECTION ====================== # ====================== DATABASE CONNECTION ======================
# Redis Cache Configuration # Redis Cache Configuration
...@@ -133,7 +137,6 @@ STARROCKS_PASSWORD: str | None = os.getenv("STARROCKS_PASSWORD") ...@@ -133,7 +137,6 @@ STARROCKS_PASSWORD: str | None = os.getenv("STARROCKS_PASSWORD")
STARROCKS_DB: str | None = os.getenv("STARROCKS_DB") STARROCKS_DB: str | None = os.getenv("STARROCKS_DB")
# Placeholder for backward compatibility if needed # Placeholder for backward compatibility if needed
AI_MODEL_NAME = DEFAULT_MODEL
# ====================== OPENTELEMETRY CONFIGURATION ====================== # ====================== OPENTELEMETRY CONFIGURATION ======================
OTEL_EXPORTER_JAEGER_AGENT_HOST = os.getenv("OTEL_EXPORTER_JAEGER_AGENT_HOST") OTEL_EXPORTER_JAEGER_AGENT_HOST = os.getenv("OTEL_EXPORTER_JAEGER_AGENT_HOST")
OTEL_EXPORTER_JAEGER_AGENT_PORT = os.getenv("OTEL_EXPORTER_JAEGER_AGENT_PORT") OTEL_EXPORTER_JAEGER_AGENT_PORT = os.getenv("OTEL_EXPORTER_JAEGER_AGENT_PORT")
......
No preview for this file type
...@@ -94,6 +94,7 @@ Pygments==2.19.2 ...@@ -94,6 +94,7 @@ Pygments==2.19.2
PyMySQL==1.1.2 PyMySQL==1.1.2
pyscn==1.5.5 pyscn==1.5.5
pytest==9.0.2 pytest==9.0.2
PyJWT==2.10.1
python-dotenv==1.2.1 python-dotenv==1.2.1
python-multipart==0.0.20 python-multipart==0.0.20
python-engineio==4.12.3 python-engineio==4.12.3
......
VITE_CLERK_PUBLISHABLE_KEY=pk_test_Y29tbXVuYWwtc3VuYmVhbS0wLmNsZXJrLmFjY291bnRzLmRldiQ
CLERK_SECRET_KEY=sk_test_ek7ozVR80Qi9UdvhGaTmlXovS16GDuBDlDrpH1rkyQ
\ No newline at end of file
This diff is collapsed.
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
"format": "biome format --write src" "format": "biome format --write src"
}, },
"dependencies": { "dependencies": {
"@clerk/clerk-react": "^6.36.8",
"@connectrpc/connect": "^2.1.1", "@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-web": "^2.1.1", "@connectrpc/connect-web": "^2.1.1",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
......
...@@ -6,6 +6,7 @@ import { Badge } from "@/components/ui/badge"; ...@@ -6,6 +6,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { getClerkSessionToken } from "@/utils/clerk";
import { generateUUID } from "@/utils/uuid"; import { generateUUID } from "@/utils/uuid";
type ProductSummary = { type ProductSummary = {
...@@ -26,6 +27,10 @@ type ChatMessage = { ...@@ -26,6 +27,10 @@ type ChatMessage = {
const DEVICE_STORAGE_KEY = "canifa_device_id"; const DEVICE_STORAGE_KEY = "canifa_device_id";
// Call backend directly (bypass Vite proxy).
// Override via VITE_API_BASE_URL, e.g. "http://localhost:5000"
const API_ORIGIN = (import.meta.env.VITE_API_BASE_URL as string | undefined) || "http://localhost:5000";
const getDeviceId = (): string => { const getDeviceId = (): string => {
if (typeof window === "undefined") return "unknown"; if (typeof window === "undefined") return "unknown";
try { try {
...@@ -90,12 +95,12 @@ const ChatbotPanel = ({ className, variant = "inline", onClose }: ChatbotPanelPr ...@@ -90,12 +95,12 @@ const ChatbotPanel = ({ className, variant = "inline", onClose }: ChatbotPanelPr
"Content-Type": "application/json", "Content-Type": "application/json",
device_id: deviceId, device_id: deviceId,
}; };
const token = getAccessToken(); const token = (await getClerkSessionToken()) || getAccessToken();
if (token) { if (token) {
headers["Authorization"] = `Bearer ${token}`; headers["Authorization"] = `Bearer ${token}`;
} }
const response = await fetch("/api/agent/chat", { const response = await fetch(`${API_ORIGIN}/api/agent/chat`, {
method: "POST", method: "POST",
headers, headers,
credentials: "include", credentials: "include",
......
...@@ -27,8 +27,12 @@ import { ...@@ -27,8 +27,12 @@ import {
type UserWebhook, type UserWebhook,
} from "./types/proto/api/v1/user_service_pb"; } from "./types/proto/api/v1/user_service_pb";
import { redirectOnAuthFailure } from "./utils/auth-redirect"; import { redirectOnAuthFailure } from "./utils/auth-redirect";
import { getClerkSessionToken } from "./utils/clerk";
const API_BASE = "/api/v1"; // Call backend directly (bypass Vite proxy).
// Override via VITE_API_BASE_URL, e.g. "http://localhost:5000"
const API_ORIGIN = (import.meta.env.VITE_API_BASE_URL as string | undefined) || "http://localhost:5000";
const API_BASE = `${API_ORIGIN}/api/v1`;
type ApiMemo = { type ApiMemo = {
id: number; id: number;
...@@ -102,7 +106,9 @@ const parseBody = async (response: Response): Promise<unknown> => { ...@@ -102,7 +106,9 @@ const parseBody = async (response: Response): Promise<unknown> => {
}; };
const fetchJson = async <T>(path: string, options: RequestOptions = {}): Promise<T> => { const fetchJson = async <T>(path: string, options: RequestOptions = {}): Promise<T> => {
const token = getAccessToken(); // Prefer Clerk token if available; fallback to legacy token store.
const clerkToken = await getClerkSessionToken();
const token = clerkToken || getAccessToken();
const headers = new Headers(options.headers); const headers = new Headers(options.headers);
if (token) { if (token) {
headers.set("Authorization", `Bearer ${token}`); headers.set("Authorization", `Bearer ${token}`);
...@@ -368,7 +374,7 @@ export const instanceServiceClient = { ...@@ -368,7 +374,7 @@ export const instanceServiceClient = {
async getInstanceProfile(_request?: unknown): Promise<InstanceProfile> { async getInstanceProfile(_request?: unknown): Promise<InstanceProfile> {
void _request; void _request;
const data = await fetchJson<ApiInstanceInfo>("/instance", { method: "GET" }); const data = await fetchJson<ApiInstanceInfo>("/instance", { method: "GET" });
const instanceUrl = typeof window === "undefined" ? "" : window.location.origin; const instanceUrl = API_ORIGIN;
return { return {
owner: "users/1", owner: "users/1",
version: typeof data?.version === "string" ? data.version : "", version: typeof data?.version === "string" ? data.version : "",
......
import { Suspense, useEffect, useMemo } from "react"; import { Suspense, useEffect, useMemo } from "react";
import { Outlet, useLocation, useSearchParams } from "react-router-dom"; import { Outlet, useLocation, useSearchParams, Link } from "react-router-dom";
import usePrevious from "react-use/lib/usePrevious"; import usePrevious from "react-use/lib/usePrevious";
import { SignedIn, SignedOut, UserButton } from "@clerk/clerk-react";
import Navigation from "@/components/Navigation"; import Navigation from "@/components/Navigation";
import ChatbotWidget from "@/components/ChatbotWidget"; import ChatbotWidget from "@/components/ChatbotWidget";
import Spinner from "@/components/Spinner"; import Spinner from "@/components/Spinner";
...@@ -58,6 +59,19 @@ const RootLayout = () => { ...@@ -58,6 +59,19 @@ const RootLayout = () => {
<Outlet /> <Outlet />
</Suspense> </Suspense>
</main> </main>
<div className="fixed top-4 right-4 z-50">
<SignedOut>
<Link
to="/auth"
className="rounded-full bg-primary px-3 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90"
>
Sign in
</Link>
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</div>
<ChatbotWidget /> <ChatbotWidget />
</div> </div>
); );
......
import "@github/relative-time-element"; import "@github/relative-time-element";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { ClerkProvider } from "@clerk/clerk-react";
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
...@@ -51,8 +52,31 @@ function AppInitializer({ children }: { children: React.ReactNode }) { ...@@ -51,8 +52,31 @@ function AppInitializer({ children }: { children: React.ReactNode }) {
} }
function Main() { function Main() {
const publishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined;
if (!publishableKey) {
return (
<div className="w-full min-h-svh flex items-center justify-center p-6">
<div className="max-w-xl w-full rounded-xl border border-border bg-background p-6 space-y-3">
<h1 className="text-xl font-semibold">Missing Clerk publishable key</h1>
<p className="text-sm text-muted-foreground">
App requires <code className="px-1 py-0.5 rounded bg-muted">VITE_CLERK_PUBLISHABLE_KEY</code>.
</p>
<div className="text-sm space-y-2">
<p>Create file <code className="px-1 py-0.5 rounded bg-muted">frontend/.env</code> with:</p>
<pre className="whitespace-pre-wrap rounded bg-muted p-3 text-xs">
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
VITE_API_BASE_URL=http://localhost:5000
</pre>
<p>Then restart Vite dev server.</p>
</div>
</div>
</div>
);
}
return ( return (
<ErrorBoundary> <ErrorBoundary>
<ClerkProvider publishableKey={publishableKey}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<InstanceProvider> <InstanceProvider>
<AuthProvider> <AuthProvider>
...@@ -65,6 +89,7 @@ function Main() { ...@@ -65,6 +89,7 @@ function Main() {
</AuthProvider> </AuthProvider>
</InstanceProvider> </InstanceProvider>
</QueryClientProvider> </QueryClientProvider>
</ClerkProvider>
</ErrorBoundary> </ErrorBoundary>
); );
} }
......
import { SignedIn, SignedOut, SignIn } from "@clerk/clerk-react";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const AuthPage = () => {
const navigate = useNavigate();
// Nếu đã đăng nhập rồi thì đưa về trang chủ
useEffect(() => {
// nhỏ gọn: SignedIn phía dưới cũng handle, đây chỉ là fallback
}, [navigate]);
return (
<div className="w-full min-h-svh flex items-center justify-center bg-background px-4">
<div className="max-w-md w-full flex flex-col items-center gap-6">
<SignedOut>
<SignIn
routing="path"
path="/auth"
signUpUrl="/auth"
redirectUrl="/"
/>
</SignedOut>
<SignedIn>
<button
type="button"
className="rounded-full bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90"
onClick={() => navigate("/")}
>
You are already signed in – Go to app
</button>
</SignedIn>
</div>
</div>
);
};
export default AuthPage;
...@@ -17,6 +17,7 @@ const Attachments = lazy(() => import("@/pages/Attachments")); ...@@ -17,6 +17,7 @@ const Attachments = lazy(() => import("@/pages/Attachments"));
const Setting = lazy(() => import("@/pages/Setting")); const Setting = lazy(() => import("@/pages/Setting"));
const UserProfile = lazy(() => import("@/pages/UserProfile")); const UserProfile = lazy(() => import("@/pages/UserProfile"));
const MemoDetailRedirect = lazy(() => import("./MemoDetailRedirect")); const MemoDetailRedirect = lazy(() => import("./MemoDetailRedirect"));
const AuthPage = lazy(() => import("@/pages/Auth"));
import { ROUTES } from "./routes"; import { ROUTES } from "./routes";
...@@ -55,6 +56,7 @@ const router = createBrowserRouter([ ...@@ -55,6 +56,7 @@ const router = createBrowserRouter([
{ path: "u/:username", element: <LazyRoute component={UserProfile} /> }, { path: "u/:username", element: <LazyRoute component={UserProfile} /> },
], ],
}, },
{ path: Routes.AUTH, element: <LazyRoute component={AuthPage} /> },
{ path: Routes.ATTACHMENTS, element: <LazyRoute component={Attachments} /> }, { path: Routes.ATTACHMENTS, element: <LazyRoute component={Attachments} /> },
{ path: Routes.INBOX, element: <LazyRoute component={Inboxes} /> }, { path: Routes.INBOX, element: <LazyRoute component={Inboxes} /> },
{ path: Routes.SETTING, element: <LazyRoute component={Setting} /> }, { path: Routes.SETTING, element: <LazyRoute component={Setting} /> },
......
declare global {
interface Window {
Clerk?: {
session?: {
getToken: (options?: Record<string, unknown>) => Promise<string>;
};
};
}
}
/**
* Get a Clerk session JWT without React hooks (works from non-React modules).
* Returns null if Clerk isn't initialized or user isn't signed in.
*/
export async function getClerkSessionToken(): Promise<string | null> {
try {
const clerk = typeof window === "undefined" ? undefined : window.Clerk;
if (!clerk?.session?.getToken) return null;
const token = await clerk.session.getToken();
return token || null;
} catch {
return null;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment