From c3819d0d6acbf2b363fea29d95658ffb5a2a8dfe Mon Sep 17 00:00:00 2001 From: Yee Kit Date: Wed, 28 Feb 2024 10:26:46 +0800 Subject: [PATCH 01/40] Bugfix: Not redirecting to page URL the sign-in is initiated from. --- frontend/app/components/ui/login-buttons.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/app/components/ui/login-buttons.tsx b/frontend/app/components/ui/login-buttons.tsx index ace7e7d..d9c7d90 100644 --- a/frontend/app/components/ui/login-buttons.tsx +++ b/frontend/app/components/ui/login-buttons.tsx @@ -5,6 +5,7 @@ import { signIn } from 'next-auth/react' import { cn } from '@/app/components/ui/lib/utils' import { Button, type ButtonProps } from '@/app/components/ui/button' import { IconGoogle, IconSGid, IconSpinner } from '@/app/components/ui/icons' +import { useSearchParams } from 'next/navigation' interface LoginButtonProps extends ButtonProps { showIcon?: boolean; @@ -18,13 +19,14 @@ function GoogleLoginButton({ ...props }: LoginButtonProps) { const [isLoading, setIsLoading] = useState(false); - + const searchParams = useSearchParams() + const callbackURL = searchParams.get("callbackUrl"); // Get the 'callbackURL' query parameter return (
- +
Home
- +
About
- +
Chat
- +
Q&A
- +
Search @@ -169,7 +179,7 @@ export default function Header() {
{/* Status Page Button/Indicator */} API: - +
{isLoading ? ( @@ -201,8 +211,46 @@ export default function Header() { )} + + + + {/* Conditionally render the user profile and logout buttons based on the user's authentication status */} + {status === 'loading' ? ( +
+ +
+ ) : session ? ( + <> + {/* User Profile Button */} + +
+ +
+
+ + {/* Sign Out Button */} + + + ) : ( + +
+ + Sign In +
+
+ )}
+ {/* Mobile menu component */} < MobileMenu isOpen={isMobileMenuOpen} onClose={() => setMobileMenuOpen(false) } logoSrc={logo} items={MobileMenuItems} /> diff --git a/frontend/app/components/query-section.tsx b/frontend/app/components/query-section.tsx index b9615ce..55d7f5e 100644 --- a/frontend/app/components/query-section.tsx +++ b/frontend/app/components/query-section.tsx @@ -3,8 +3,10 @@ import { useChat } from "ai/react"; import { ChatInput, ChatMessages } from "@/app/components/ui/chat"; import AutofillQuestion from "@/app/components/ui/autofill-prompt/autofill-prompt-dialog"; +import { useSession } from "next-auth/react"; export default function QuerySection() { + const { data: session } = useSession(); const { messages, input, @@ -13,7 +15,13 @@ export default function QuerySection() { handleInputChange, reload, stop, - } = useChat({ api: process.env.NEXT_PUBLIC_QUERY_API }); + } = useChat({ + api: process.env.NEXT_PUBLIC_QUERY_API, + // Add the access token to the request headers + headers: { + 'Authorization': `Bearer ${session?.supabaseAccessToken}`, + } + }); return (
diff --git a/frontend/app/components/ui/mobilemenu.tsx b/frontend/app/components/ui/mobilemenu.tsx index 97e38ad..8f9ff32 100644 --- a/frontend/app/components/ui/mobilemenu.tsx +++ b/frontend/app/components/ui/mobilemenu.tsx @@ -70,7 +70,7 @@ const MobileMenu: React.FC = ({ isOpen, onClose, logoSrc, items {/* Mobile menu content */}
{items.map((item, index) => ( - +
{item.icon} {item.label} diff --git a/frontend/app/components/ui/navlink.tsx b/frontend/app/components/ui/navlink.tsx index 59efbae..759a736 100644 --- a/frontend/app/components/ui/navlink.tsx +++ b/frontend/app/components/ui/navlink.tsx @@ -5,12 +5,13 @@ import Link from 'next/link'; export interface NavLinkProps { href: string; + title: string; children: React.ReactNode; onClick?: () => void; // Include onClick as an optional prop target?: string; } -const HeaderNavLink: React.FC = ({ href, children, onClick }) => { +const HeaderNavLink: React.FC = ({ href, title, children, onClick }) => { // Use the useRouter hook to get information about the current route const pathname = usePathname(); @@ -24,7 +25,7 @@ const HeaderNavLink: React.FC = ({ href, children, onClick }) => { }; return ( - + {/* Add a class to highlight the active tab */}
{children} diff --git a/frontend/app/components/ui/search/useSearch.tsx b/frontend/app/components/ui/search/useSearch.tsx index 67cb88a..f79e984 100644 --- a/frontend/app/components/ui/search/useSearch.tsx +++ b/frontend/app/components/ui/search/useSearch.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { SearchResult } from "@/app/components/ui/search/search.interface"; +import { useSession } from 'next-auth/react'; interface UseSearchResult { searchResults: SearchResult[]; @@ -15,6 +16,9 @@ const useSearch = (): UseSearchResult => { const [searchResults, setSearchResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSearchButtonPressed, setIsSearchButtonPressed] = useState(false); + const { data: session, status } = useSession(); + // console.log('session:', session, 'status:', status); + const supabaseAccessToken = session?.supabaseAccessToken; const handleSearch = async (query: string): Promise => { setIsSearchButtonPressed(isSearchButtonPressed); @@ -38,6 +42,10 @@ const useSearch = (): UseSearchResult => { } const response = await fetch(`${search_api}?query=${query}`, { signal: AbortSignal.timeout(120000), // Abort the request if it takes longer than 120 seconds + // Add the access token to the request headers + headers: { + 'Authorization': `Bearer ${supabaseAccessToken}`, + } }); const data = await response.json(); setSearchResults(data); diff --git a/frontend/app/status/page.tsx b/frontend/app/status/page.tsx index 751de1e..f12602c 100644 --- a/frontend/app/status/page.tsx +++ b/frontend/app/status/page.tsx @@ -3,18 +3,27 @@ import useSWR from 'swr'; import { Button } from "@nextui-org/react"; import { IconSpinner } from '@/app/components/ui/icons'; +import { useSession } from 'next-auth/react'; // Define the API endpoint -const healthcheck_api = process.env.NEXT_PUBLIC_HEALTHCHECK_API; +const healthcheck_api = "/api/status"; const StatusPage = () => { + const { data: session, status } = useSession(); + const supabaseAccessToken = session?.supabaseAccessToken; + // console.log('supabaseAccessToken:', supabaseAccessToken); + // Use SWR hook to fetch data with caching and revalidation const { data, error, isValidating, mutate } = useSWR(healthcheck_api, async (url) => { try { // Fetch the data const response = await fetch(url, { signal: AbortSignal.timeout(5000), // Abort the request if it takes longer than 5 seconds - }); + // Add the access token to the request headers + headers: { + 'Authorization': `Bearer ${supabaseAccessToken}`, + } + }); if (!response.ok) { throw new Error(response.statusText || 'Unknown Error'); } diff --git a/frontend/auth.ts b/frontend/auth.ts index bbba090..3dcb2bb 100644 --- a/frontend/auth.ts +++ b/frontend/auth.ts @@ -53,7 +53,8 @@ const decryptData = async (encKey: string, block: { [s: string]: unknown; } | Ar // to `NextAuth` in `app/api/auth/[...nextauth]/route.ts` export const config = { // Configure one or more authentication providers - debug: true, // Enable debug messages in the console if you are having problems + // Enable debug messages if running in development + debug: process.env.NODE_ENV === 'development', providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID as string, @@ -133,7 +134,7 @@ export const config = { aud: "authenticated", exp: Math.floor(new Date(session.expires).getTime() / 1000), sub: user.id, - email: user.email, + // email: user.email, role: "authenticated", } session.supabaseAccessToken = jwt.sign(payload, signingSecret) diff --git a/frontend/middleware.ts b/frontend/middleware.ts index b525b60..ad8ab4b 100644 --- a/frontend/middleware.ts +++ b/frontend/middleware.ts @@ -1,14 +1,3 @@ -// export { default } from "next-auth/middleware" - -// export const config = { -// matcher: ["/chat", "/search", "/query"] -// } - -// // Ensure auth is required for all except the following paths -// export const config = { -// matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico|favicon-16x16.png|apple-touch-icon.png|about|sign-in|api/status|privacy-policy|terms-of-service|sitemap.xml|robots.txt).+)'] -// }; - import { User } from "next-auth"; import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@supabase/supabase-js" @@ -16,58 +5,70 @@ import { createClient } from "@supabase/supabase-js" export const middleware = async (request: NextRequest) => { const { pathname, origin } = request.nextUrl; const signinPage = new URL('/sign-in', origin); - // add params to the signinPage URL + // Add callbackUrl params to the signinPage URL signinPage.searchParams.set('callbackUrl', pathname); + // Retrieve the session token from the request cookies const session = request.cookies.get('next-auth.session-token'); - let sessionExpired = false; - if (!session) { - return NextResponse.redirect(signinPage.href, { status: 302 }); - } - // console.log('session:', session); + if (session) { + // console.log('session:', session); - // Check the database for the session token - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL ?? '', - process.env.SUPABASE_SERVICE_ROLE_KEY ?? '', - { db: { schema: 'next_auth' } }, - ); + // Check the database for the session token + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL ?? '', + process.env.SUPABASE_SERVICE_ROLE_KEY ?? '', + { db: { schema: 'next_auth' } }, + ); - const { data, error } = await supabase - .from('sessions') - .select('userId, expires') - .eq('sessionToken', session?.value) - .single(); + const { data, error } = await supabase + .from('sessions') + .select('userId, expires') + .eq('sessionToken', session?.value) + .single(); - if (!error) { // console.log('data:', data); - // Check if the session is expired + // Check if the session is expired or not const now = new Date().getTime(); const expires = new Date(data?.expires).getTime(); - sessionExpired = expires > now ? true : false; + const sessionExpired = expires > now ? true : false; + + // Redirect to the sign-in page if the session is expired and not on the sign-in page + if (pathname != "/sign-in" && !sessionExpired) { + return NextResponse.redirect(signinPage.href, { status: 302 }); + } - // Set a cookie to be used by the frontend to check if the session is expired - // request.cookies.set('next-auth.session-valid', true, { - // httpOnly: true, - // sameSite: 'lax', - // secure: true, - // path: '/', - // maxAge: 60 * 60 * 24 * 1 // 1 day - // }); + if (error) { + // Redirect to the sign-in page if there is an error fetching the session from the database + console.error('Error fetching session from database:', error.message); + return NextResponse.redirect(signinPage.href, { status: 302 }); + } } else { - console.error('Error fetching session from database:', error.message); - } - - // Redirect to the sign-in page if the session is expired - if (pathname != "/sign-in" && !sessionExpired) { + // Redirect to the sign-in page if there is no session token + // console.error('No session token found'); return NextResponse.redirect(signinPage.href, { status: 302 }); } + + // Continue to the next middleware + return NextResponse.next(); } export const config = { matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico|favicon-16x16.png|apple-touch-icon.png|about|sign-in|api/status|privacy-policy|terms-of-service|sitemap.xml|robots.txt).+)'] } -type session = {} | User; \ No newline at end of file +type session = {} | User; + +// Default middleware for NextAuth checking for JWT session not database session + +// export { default } from "next-auth/middleware" + +// export const config = { +// matcher: ["/chat", "/search", "/query"] +// } + +// // Ensure auth is required for all except the following paths +// export const config = { +// matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico|favicon-16x16.png|apple-touch-icon.png|about|sign-in|api/status|privacy-policy|terms-of-service|sitemap.xml|robots.txt).+)'] +// }; \ No newline at end of file From 648622301585087ef219eeb63491e866c43cc90c Mon Sep 17 00:00:00 2001 From: Yee Kit Date: Fri, 22 Mar 2024 16:04:36 +0800 Subject: [PATCH 09/40] Bugfix: Slicing results only when results is not empty --- .../app/components/ui/search/search-results.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/app/components/ui/search/search-results.tsx b/frontend/app/components/ui/search/search-results.tsx index aaeb230..7ec6deb 100644 --- a/frontend/app/components/ui/search/search-results.tsx +++ b/frontend/app/components/ui/search/search-results.tsx @@ -11,14 +11,20 @@ const SearchResults: React.FC = ({ query, results, isLoading, sea // Sort results by similarity score whenever results or query change useEffect(() => { - if (query.trim() === "" && !searchButtonPressed){ + if (query.trim() === "" && !searchButtonPressed) { // Reset sortedResults when query is empty setSortedResults([]); } else if (query.trim() !== "" && searchButtonPressed) { - // Sort results by similarity score - const sorted = results.slice().sort((a, b) => b.similarity_score - a.similarity_score); - // Update sortedResults state - setSortedResults(sorted); + // if results are empty + if (results.length === 0) { + setSortedResults([]); + } + else { + // Sort results by similarity score + const sorted = results.slice().sort((a, b) => b.similarity_score - a.similarity_score); + // Update sortedResults state + setSortedResults(sorted); + } } }, [query, results]); From 9867448eabbd48d5fedce19d6379fcf094af4c99 Mon Sep 17 00:00:00 2001 From: Yee Kit Date: Fri, 22 Mar 2024 16:04:49 +0800 Subject: [PATCH 10/40] Profile Page Placeholder --- frontend/app/profile/page.tsx | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 frontend/app/profile/page.tsx diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx new file mode 100644 index 0000000..d2a5271 --- /dev/null +++ b/frontend/app/profile/page.tsx @@ -0,0 +1,54 @@ +"use client"; + +import React, { useState } from 'react'; + +const ProfilePage: React.FC = () => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [bio, setBio] = useState(''); + + const handleNameChange = (event: React.ChangeEvent) => { + setName(event.target.value); + }; + + const handleEmailChange = (event: React.ChangeEvent) => { + setEmail(event.target.value); + }; + + const handleBioChange = (event: React.ChangeEvent) => { + setBio(event.target.value); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // TODO: Handle form submission logic + }; + + return ( +
+
+

Profile Settings

+
+ +
+ +
+