From 4622d73bc05985c414b78c6b5e0569212fa2d32e Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Fri, 6 Dec 2024 12:19:42 +0545 Subject: [PATCH 01/15] feat: auth v1 --- src/app/test/page.tsx | 12 ++ src/components/auth_v1/AuthFlow.tsx | 186 ++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 src/app/test/page.tsx create mode 100644 src/components/auth_v1/AuthFlow.tsx diff --git a/src/app/test/page.tsx b/src/app/test/page.tsx new file mode 100644 index 00000000..2b15eb12 --- /dev/null +++ b/src/app/test/page.tsx @@ -0,0 +1,12 @@ +import AuthFlow from "@/components/auth_v1/AuthFlow"; +import React from "react"; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/src/components/auth_v1/AuthFlow.tsx b/src/components/auth_v1/AuthFlow.tsx new file mode 100644 index 00000000..5e2949bf --- /dev/null +++ b/src/components/auth_v1/AuthFlow.tsx @@ -0,0 +1,186 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { showConnect, openSignatureRequestPopup } from "@stacks/connect"; +import { AppConfig, UserSession } from "@stacks/auth"; +import { StacksMainnet } from "@stacks/network"; +import { verifyMessageSignatureRsv } from "@stacks/encryption"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +const appConfig = new AppConfig(["store_write", "publish_data"]); +const userSession = new UserSession({ appConfig }); + +const FRONTEND_SECRET_KEY = "c4cf807d-acb2-497c-84e8-3098957b5339"; +const API_BASE_URL = "https://services.aibtc.dev/auth"; + +export default function AuthFlow() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + checkSessionToken(); + }, []); + + const checkSessionToken = async () => { + const sessionToken = localStorage.getItem("sessionToken"); + if (sessionToken) { + try { + const response = await fetch(`${API_BASE_URL}/verify-session-token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: FRONTEND_SECRET_KEY, + }, + body: JSON.stringify({ data: sessionToken }), + }); + + const data = await response.json(); + if (response.ok) { + setIsAuthenticated(true); + } else { + localStorage.removeItem("sessionToken"); + setError(data.error || "Session verification failed"); + } + } catch (error) { + console.error("Error verifying session token:", error); + setError("Failed to verify session. Please try again."); + } + } + setIsLoading(false); + }; + + const authenticate = () => { + showConnect({ + appDetails: { + name: "sprint.aibtc.dev", + icon: window.location.origin + "/app-icon.png", + }, + redirectTo: "/", + onFinish: () => { + const userData = userSession.loadUserData(); + promptSignMessage(userData.profile.stxAddress.mainnet); + }, + userSession: userSession, + }); + }; + + const promptSignMessage = (stxAddress: string) => { + const message = "Welcome to aibtcdev!"; + + openSignatureRequestPopup({ + message, + network: new StacksMainnet(), + appDetails: { + name: "sprint.aibtc.dev", + icon: window.location.origin + "/app-icon.png", + }, + stxAddress, + onFinish: (data) => + verifyAndSendSignedMessage( + message, + data.signature, + data.publicKey, + stxAddress + ), + onCancel: () => { + setError("Message signing was cancelled."); + }, + }); + }; + + const verifyAndSendSignedMessage = async ( + message: string, + signature: string, + publicKey: string, + stxAddress: string + ) => { + try { + // Optional: Verify signature locally before sending + const isSignatureValid = verifyMessageSignatureRsv({ + message, + signature, + publicKey, + }); + + if (!isSignatureValid) { + setError("Signature verification failed"); + return; + } + + const response = await fetch(`${API_BASE_URL}/request-auth-token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: FRONTEND_SECRET_KEY, + }, + body: JSON.stringify({ + data: signature, // Send just the signature as the backend expects + }), + }); + + const data = await response.json(); + + if (response.ok) { + localStorage.setItem("sessionToken", data.sessionToken); + setIsAuthenticated(true); + setError(null); + } else { + console.error("Auth Error:", data); + setError(data.error || "Authentication failed. Please try again."); + } + } catch (error) { + console.error("Error getting auth token:", error); + setError("Authentication failed. Please try again."); + } + }; + + const handleLogout = () => { + localStorage.removeItem("sessionToken"); + setIsAuthenticated(false); + }; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+ + + Authentication Flow + + Connect your wallet and authenticate + + + + {!isAuthenticated ? ( + + ) : ( +
+

+ Authenticated Successfully! +

+ +
+ )} + {error &&

{error}

} +
+
+
+ ); +} From 4fc416a8e370b1e7bdc1d58b0abd7103765bd23a Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Fri, 6 Dec 2024 12:59:52 +0545 Subject: [PATCH 02/15] feat: create auth helper --- src/components/auth_v1/AuthFlow.tsx | 161 +++++++------------------- src/helpers/authHelpers.ts | 169 ++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+), 122 deletions(-) create mode 100644 src/helpers/authHelpers.ts diff --git a/src/components/auth_v1/AuthFlow.tsx b/src/components/auth_v1/AuthFlow.tsx index 5e2949bf..07ed6713 100644 --- a/src/components/auth_v1/AuthFlow.tsx +++ b/src/components/auth_v1/AuthFlow.tsx @@ -1,9 +1,6 @@ "use client"; + import React, { useState, useEffect } from "react"; -import { showConnect, openSignatureRequestPopup } from "@stacks/connect"; -import { AppConfig, UserSession } from "@stacks/auth"; -import { StacksMainnet } from "@stacks/network"; -import { verifyMessageSignatureRsv } from "@stacks/encryption"; import { Button } from "@/components/ui/button"; import { Card, @@ -12,12 +9,12 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; - -const appConfig = new AppConfig(["store_write", "publish_data"]); -const userSession = new UserSession({ appConfig }); - -const FRONTEND_SECRET_KEY = "c4cf807d-acb2-497c-84e8-3098957b5339"; -const API_BASE_URL = "https://services.aibtc.dev/auth"; +import { + checkSessionToken, + initiateAuthentication, + promptSignMessage, + logout, +} from "@/helpers/authHelpers"; export default function AuthFlow() { const [isAuthenticated, setIsAuthenticated] = useState(false); @@ -25,125 +22,44 @@ export default function AuthFlow() { const [error, setError] = useState(null); useEffect(() => { - checkSessionToken(); - }, []); - - const checkSessionToken = async () => { - const sessionToken = localStorage.getItem("sessionToken"); - if (sessionToken) { + const verifySession = async () => { try { - const response = await fetch(`${API_BASE_URL}/verify-session-token`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: FRONTEND_SECRET_KEY, - }, - body: JSON.stringify({ data: sessionToken }), - }); - - const data = await response.json(); - if (response.ok) { + const authResult = await checkSessionToken(); + if (authResult) { setIsAuthenticated(true); - } else { - localStorage.removeItem("sessionToken"); - setError(data.error || "Session verification failed"); + console.log("STX Address:", authResult.stxAddress); + console.log("Session Token:", authResult.sessionToken); } - } catch (error) { - console.error("Error verifying session token:", error); - setError("Failed to verify session. Please try again."); + } catch (err) { + console.error("Session verification error:", err); + } finally { + setIsLoading(false); } - } - setIsLoading(false); - }; - - const authenticate = () => { - showConnect({ - appDetails: { - name: "sprint.aibtc.dev", - icon: window.location.origin + "/app-icon.png", - }, - redirectTo: "/", - onFinish: () => { - const userData = userSession.loadUserData(); - promptSignMessage(userData.profile.stxAddress.mainnet); - }, - userSession: userSession, - }); - }; - - const promptSignMessage = (stxAddress: string) => { - const message = "Welcome to aibtcdev!"; + }; - openSignatureRequestPopup({ - message, - network: new StacksMainnet(), - appDetails: { - name: "sprint.aibtc.dev", - icon: window.location.origin + "/app-icon.png", - }, - stxAddress, - onFinish: (data) => - verifyAndSendSignedMessage( - message, - data.signature, - data.publicKey, - stxAddress - ), - onCancel: () => { - setError("Message signing was cancelled."); - }, - }); - }; - - const verifyAndSendSignedMessage = async ( - message: string, - signature: string, - publicKey: string, - stxAddress: string - ) => { - try { - // Optional: Verify signature locally before sending - const isSignatureValid = verifyMessageSignatureRsv({ - message, - signature, - publicKey, - }); - - if (!isSignatureValid) { - setError("Signature verification failed"); - return; - } + verifySession(); + }, []); - const response = await fetch(`${API_BASE_URL}/request-auth-token`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: FRONTEND_SECRET_KEY, + const handleAuthenticate = () => { + initiateAuthentication((address) => { + promptSignMessage( + address, + () => { + setIsAuthenticated(true); + setError(null); + console.log("Authentication successful."); }, - body: JSON.stringify({ - data: signature, // Send just the signature as the backend expects - }), - }); - - const data = await response.json(); - - if (response.ok) { - localStorage.setItem("sessionToken", data.sessionToken); - setIsAuthenticated(true); - setError(null); - } else { - console.error("Auth Error:", data); - setError(data.error || "Authentication failed. Please try again."); - } - } catch (error) { - console.error("Error getting auth token:", error); - setError("Authentication failed. Please try again."); - } + (errorMessage) => { + setError(errorMessage); + } + ); + }); }; const handleLogout = () => { - localStorage.removeItem("sessionToken"); + logout(); setIsAuthenticated(false); + console.log("Logged out successfully"); }; if (isLoading) { @@ -151,7 +67,7 @@ export default function AuthFlow() { } return ( -
+
Authentication Flow @@ -161,18 +77,19 @@ export default function AuthFlow() { {!isAuthenticated ? ( - ) : (

- Authenticated Successfully! + Authenticated Successfully! ..session and is stored in + localstorage

diff --git a/src/helpers/authHelpers.ts b/src/helpers/authHelpers.ts new file mode 100644 index 00000000..9c1b3f26 --- /dev/null +++ b/src/helpers/authHelpers.ts @@ -0,0 +1,169 @@ +import { AppConfig, UserSession } from "@stacks/auth"; +import { StacksMainnet } from "@stacks/network"; +import { showConnect, openSignatureRequestPopup } from "@stacks/connect"; +import { verifyMessageSignatureRsv } from "@stacks/encryption"; + +// Configuration constants for authentication +const FRONTEND_SECRET_KEY = "c4cf807d-acb2-497c-84e8-3098957b5339"; +const API_BASE_URL = "https://services.aibtc.dev/auth"; + +// Create an app configuration with required permissions +export const appConfig = new AppConfig(["store_write", "publish_data"]); + +// Initialize a user session with the app configuration +export const userSession = new UserSession({ appConfig }); + + +export interface AuthResult { + stxAddress: string; + sessionToken: string; +} + + +export const checkSessionToken = async (): Promise => { + // Retrieve stored session token and STX address from local storage + const storedSessionToken = localStorage.getItem("sessionToken"); + const storedStxAddress = localStorage.getItem("stxAddress"); + + // If both token and address exist, attempt to verify + if (storedSessionToken && storedStxAddress) { + try { + // Send a request to backend to verify the session token + const response = await fetch(`${API_BASE_URL}/verify-session-token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: FRONTEND_SECRET_KEY, + }, + body: JSON.stringify({ data: storedSessionToken }), + }); + + // If verification is successful, return the authentication result + if (response.ok) { + return { + stxAddress: storedStxAddress, + sessionToken: storedSessionToken + }; + } + } catch (error) { + console.error("Error verifying session token:", error); + } + } + return null; +}; + + +export const initiateAuthentication = (onSuccess: (address: string) => void) => { + showConnect({ + + appDetails: { + name: "sprint.aibtc.dev", + icon: window.location.origin + "/app-icon.png", + }, + redirectTo: "/", + onFinish: () => { + const userData = userSession.loadUserData(); + onSuccess(userData.profile.stxAddress.mainnet); + }, + userSession: userSession, + }); +}; + +export const promptSignMessage = ( + stxAddress: string, + onSuccess: (result: AuthResult) => void, + onError: (error: string) => void +) => { + // Predefined message to be signed + const message = "Welcome to aibtcdev!"; + + // Open signature request popup using Stacks Connect + openSignatureRequestPopup({ + message, + network: new StacksMainnet(), + appDetails: { + name: "sprint.aibtc.dev", + icon: window.location.origin + "/app-icon.png", + }, + stxAddress, + // Callback when signature is completed + onFinish: (data) => verifyAndSendSignedMessage( + message, + data.signature, + data.publicKey, + stxAddress, + onSuccess, + onError + ), + // Callback if user cancels signing + onCancel: () => { + onError("Message signing was cancelled."); + }, + }); +}; + + +// Verifies the signed message and requests an authentication token from the backend +export const verifyAndSendSignedMessage = async ( + message: string, + signature: string, + publicKey: string, + stxAddress: string, + onSuccess: (result: AuthResult) => void, + onError: (error: string) => void +) => { + try { + // Locally verify the signature to ensure its validity + const isSignatureValid = verifyMessageSignatureRsv({ + message, + signature, + publicKey, + }); + + // If signature is invalid, trigger error callback + if (!isSignatureValid) { + onError("Signature verification failed"); + return; + } + + // Send signature to backend to request authentication token + const response = await fetch(`${API_BASE_URL}/request-auth-token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: FRONTEND_SECRET_KEY, + }, + body: JSON.stringify({ + data: signature, + }), + }); + + const data = await response.json(); + // Handle successful authentication + if (response.ok) { + // Store authentication details in local storage + localStorage.setItem("sessionToken", data.sessionToken); + localStorage.setItem("stxAddress", stxAddress); + + // Prepare authentication result + const authResult = { + stxAddress, + sessionToken: data.sessionToken + }; + onSuccess(authResult); + } else { + console.error("Auth Error:", data); + onError(data.error || "Authentication failed. Please try again."); + } + } catch (error) { + console.error("Error getting auth token:", error); + onError("Authentication failed. Please try again."); + } +}; + + +// Logs out the user by removing authentication tokens from local storage +export const logout = () => { + localStorage.removeItem("sessionToken"); + localStorage.removeItem("stxAddress"); +}; \ No newline at end of file From 6998882fa188810a5f047fb56a81bf151bee10cd Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Fri, 6 Dec 2024 13:04:45 +0545 Subject: [PATCH 03/15] fix: add env vars for secret --- src/helpers/authHelpers.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/helpers/authHelpers.ts b/src/helpers/authHelpers.ts index 9c1b3f26..98c6f475 100644 --- a/src/helpers/authHelpers.ts +++ b/src/helpers/authHelpers.ts @@ -3,23 +3,15 @@ import { StacksMainnet } from "@stacks/network"; import { showConnect, openSignatureRequestPopup } from "@stacks/connect"; import { verifyMessageSignatureRsv } from "@stacks/encryption"; -// Configuration constants for authentication -const FRONTEND_SECRET_KEY = "c4cf807d-acb2-497c-84e8-3098957b5339"; -const API_BASE_URL = "https://services.aibtc.dev/auth"; -// Create an app configuration with required permissions export const appConfig = new AppConfig(["store_write", "publish_data"]); - -// Initialize a user session with the app configuration export const userSession = new UserSession({ appConfig }); - export interface AuthResult { stxAddress: string; sessionToken: string; } - export const checkSessionToken = async (): Promise => { // Retrieve stored session token and STX address from local storage const storedSessionToken = localStorage.getItem("sessionToken"); @@ -29,11 +21,11 @@ export const checkSessionToken = async (): Promise => { if (storedSessionToken && storedStxAddress) { try { // Send a request to backend to verify the session token - const response = await fetch(`${API_BASE_URL}/verify-session-token`, { + const response = await fetch(`${process.env.AIBTC_SERVICE_URL}/auth/verify-session-token`, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: FRONTEND_SECRET_KEY, + Authorization: process.env.FRONTEND_SECRET_KEY!, }, body: JSON.stringify({ data: storedSessionToken }), }); @@ -127,11 +119,11 @@ export const verifyAndSendSignedMessage = async ( } // Send signature to backend to request authentication token - const response = await fetch(`${API_BASE_URL}/request-auth-token`, { + const response = await fetch(`${process.env.AIBTC_SERVICE_URL}/auth/request-auth-token`, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: FRONTEND_SECRET_KEY, + Authorization: process.env.FRONTEND_SECRET_KEY!, }, body: JSON.stringify({ data: signature, From ed91bd8a44ff29f105bd93ae07893402570e94a5 Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Fri, 6 Dec 2024 13:18:26 +0545 Subject: [PATCH 04/15] fix: env var names --- src/helpers/authHelpers.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/helpers/authHelpers.ts b/src/helpers/authHelpers.ts index 98c6f475..5d8da699 100644 --- a/src/helpers/authHelpers.ts +++ b/src/helpers/authHelpers.ts @@ -16,16 +16,17 @@ export const checkSessionToken = async (): Promise => { // Retrieve stored session token and STX address from local storage const storedSessionToken = localStorage.getItem("sessionToken"); const storedStxAddress = localStorage.getItem("stxAddress"); + console.log(process.env.NEXT_PUBLIC_AIBTC_SECRET_KEY!) // If both token and address exist, attempt to verify if (storedSessionToken && storedStxAddress) { try { // Send a request to backend to verify the session token - const response = await fetch(`${process.env.AIBTC_SERVICE_URL}/auth/verify-session-token`, { + const response = await fetch(`${process.env.NEXT_PUBLIC_AIBTC_SERVICE_URL}/auth/verify-session-token`, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: process.env.FRONTEND_SECRET_KEY!, + Authorization: process.env.NEXT_PUBLIC_AIBTC_SECRET_KEY!, }, body: JSON.stringify({ data: storedSessionToken }), }); @@ -119,11 +120,11 @@ export const verifyAndSendSignedMessage = async ( } // Send signature to backend to request authentication token - const response = await fetch(`${process.env.AIBTC_SERVICE_URL}/auth/request-auth-token`, { + const response = await fetch(`${process.env.NEXT_PUBLIC_AIBTC_SERVICE_URL}/auth/request-auth-token`, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: process.env.FRONTEND_SECRET_KEY!, + Authorization: process.env.NEXT_PUBLIC_AIBTC_SECRET_KEY!, }, body: JSON.stringify({ data: signature, From acb896b38e1fbc2013835607f8743295b30e699c Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Wed, 11 Dec 2024 09:08:53 +0545 Subject: [PATCH 05/15] fix: remove v1/auth from main --- src/app/test/page.tsx | 12 ---- src/components/auth_v1/AuthFlow.tsx | 103 ---------------------------- 2 files changed, 115 deletions(-) delete mode 100644 src/app/test/page.tsx delete mode 100644 src/components/auth_v1/AuthFlow.tsx diff --git a/src/app/test/page.tsx b/src/app/test/page.tsx deleted file mode 100644 index 2b15eb12..00000000 --- a/src/app/test/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import AuthFlow from "@/components/auth_v1/AuthFlow"; -import React from "react"; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; diff --git a/src/components/auth_v1/AuthFlow.tsx b/src/components/auth_v1/AuthFlow.tsx deleted file mode 100644 index 07ed6713..00000000 --- a/src/components/auth_v1/AuthFlow.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - checkSessionToken, - initiateAuthentication, - promptSignMessage, - logout, -} from "@/helpers/authHelpers"; - -export default function AuthFlow() { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const verifySession = async () => { - try { - const authResult = await checkSessionToken(); - if (authResult) { - setIsAuthenticated(true); - console.log("STX Address:", authResult.stxAddress); - console.log("Session Token:", authResult.sessionToken); - } - } catch (err) { - console.error("Session verification error:", err); - } finally { - setIsLoading(false); - } - }; - - verifySession(); - }, []); - - const handleAuthenticate = () => { - initiateAuthentication((address) => { - promptSignMessage( - address, - () => { - setIsAuthenticated(true); - setError(null); - console.log("Authentication successful."); - }, - (errorMessage) => { - setError(errorMessage); - } - ); - }); - }; - - const handleLogout = () => { - logout(); - setIsAuthenticated(false); - console.log("Logged out successfully"); - }; - - if (isLoading) { - return
Loading...
; - } - - return ( -
- - - Authentication Flow - - Connect your wallet and authenticate - - - - {!isAuthenticated ? ( - - ) : ( -
-

- Authenticated Successfully! ..session and is stored in - localstorage -

- -
- )} - {error &&

{error}

} -
-
-
- ); -} From ed3cb54250e96c2b10b6440fa8e5c815a9f07f4b Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Wed, 11 Dec 2024 09:27:19 +0545 Subject: [PATCH 06/15] feat: create auth hooks --- src/hooks/useAuth.ts | 109 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/hooks/useAuth.ts diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 00000000..34fd25f4 --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,109 @@ +import { useState } from 'react'; +import { AppConfig, showConnect, UserSession } from "@stacks/connect"; +import { supabase } from "@/utils/supabase/client"; +import { useToast } from "@/hooks/use-toast"; + +const appConfig = new AppConfig(["store_write", "publish_data"]); +const userSession = new UserSession({ appConfig }); + +export const useAuth = () => { + const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast(); + + const handleAuthentication = async (stxAddress: string) => { + try { + // Try to sign in first + const { error: signInError } = await supabase.auth.signInWithPassword({ + email: `${stxAddress}@stacks.id`, + password: stxAddress, + }); + + if (signInError && signInError.status === 400) { + // User doesn't exist, proceed with sign up + toast({ + description: "Creating your account...", + }); + + const { error: signUpError } = await supabase.auth.signUp({ + email: `${stxAddress}@stacks.id`, + password: stxAddress, + }); + + if (signUpError) throw signUpError; + + toast({ + description: "Successfully signed up...", + variant: "default", + }); + + return true; + } else if (signInError) { + throw signInError; + } + + toast({ + description: "Redirecting to dashboard...", + variant: "default", + }); + + return true; + } catch (error) { + console.error("Authentication error:", error); + toast({ + description: "Authentication failed. Please try again.", + variant: "destructive", + }); + return false; + } + }; + + const connectWallet = async () => { + setIsLoading(true); + try { + toast({ + description: "Connecting wallet...", + }); + + // Connect wallet + await new Promise((resolve) => { + showConnect({ + appDetails: { + name: "AIBTC Champions Sprint", + icon: window.location.origin + "/logos/aibtcdev-avatar-1000px.png", + }, + onCancel: () => { + toast({ + description: "Wallet connection cancelled.", + }); + setIsLoading(false); + }, + onFinish: () => resolve(), + userSession, + }); + }); + + const userData = userSession.loadUserData(); + const stxAddress = userData.profile.stxAddress.mainnet; + + toast({ + description: "Wallet connected. Authenticating...", + }); + + return { stxAddress, success: await handleAuthentication(stxAddress) }; + } catch (error) { + console.error("Wallet connection error:", error); + toast({ + description: "Failed to connect wallet. Please try again.", + variant: "destructive", + }); + return { stxAddress: null, success: false }; + } finally { + setIsLoading(false); + } + }; + + return { + connectWallet, + isLoading, + }; +}; \ No newline at end of file From f2fdfb35f267a57c448aef9a6ce3732f0536a281 Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Wed, 11 Dec 2024 09:40:57 +0545 Subject: [PATCH 07/15] fix: show user data only after auth --- src/app/application-layout.tsx | 65 ++++++++++-------- src/app/providers.tsx | 16 ++--- src/components/auth/StacksAuth.tsx | 106 ++--------------------------- 3 files changed, 48 insertions(+), 139 deletions(-) diff --git a/src/app/application-layout.tsx b/src/app/application-layout.tsx index 3f73ed09..592b6e37 100644 --- a/src/app/application-layout.tsx +++ b/src/app/application-layout.tsx @@ -42,6 +42,7 @@ import { useUserData } from "@/hooks/useUserData"; import { Wallet } from "lucide-react"; import SignOut from "@/components/auth/SignOut"; import Image from "next/image"; +import { supabase } from "@/utils/supabase/client"; function AccountDropdownMenu({ anchor, @@ -181,41 +182,45 @@ export function ApplicationLayout({ children }: { children: React.ReactNode }) { Terms of Service - - - - {isLoading ? "Loading..." : displayAgentAddress} - {userData?.agentAddress && ( - - {userData.agentBalance !== null - ? `${userData.agentBalance.toFixed(5)} STX` - : "Loading balance..."} - - )} - - + {userData && !isLoading && ( + + + + {isLoading ? "Loading..." : displayAgentAddress} + {userData?.agentAddress && ( + + {userData.agentBalance !== null + ? `${userData.agentBalance.toFixed(5)} STX` + : "Loading balance..."} + + )} + + + )} - - - - - - - - {isLoading ? "Loading..." : displayAddress} - - - {isLoading ? "Loading..." : displayRole} + {userData && !isLoading && ( + + + + + + + + {isLoading ? "Loading..." : displayAddress} + + + {isLoading ? "Loading..." : displayRole} + - - - - - - + + + + + + )} } > diff --git a/src/app/providers.tsx b/src/app/providers.tsx index f7f3fcd8..87e720e7 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -10,14 +10,14 @@ import { usePathname } from "next/navigation"; const queryClient = new QueryClient(); export function Providers({ children }: { children: React.ReactNode }) { - const pathname = usePathname(); + // const pathname = usePathname(); - const content = - pathname === "/" ? ( - children - ) : ( - {children} - ); + // const content = + // pathname === "/" ? ( + // children + // ) : ( + // {children} + // ); return ( - {content} + {children} diff --git a/src/components/auth/StacksAuth.tsx b/src/components/auth/StacksAuth.tsx index 538c88d5..b0c22435 100644 --- a/src/components/auth/StacksAuth.tsx +++ b/src/components/auth/StacksAuth.tsx @@ -1,121 +1,25 @@ "use client"; import React, { useState, useEffect } from "react"; -import { AppConfig, showConnect, UserSession } from "@stacks/connect"; -import { supabase } from "@/utils/supabase/client"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; -import { useToast } from "@/hooks/use-toast"; - -const appConfig = new AppConfig(["store_write", "publish_data"]); -const userSession = new UserSession({ appConfig }); +import { useAuth } from "@/hooks/useAuth"; export default function StacksAuth() { const [mounted, setMounted] = useState(false); - const [isLoading, setIsLoading] = useState(false); + const { connectWallet, isLoading } = useAuth(); const router = useRouter(); - const { toast } = useToast(); useEffect(() => { setMounted(true); }, []); - const handleAuthentication = async (stxAddress: string) => { - try { - // Try to sign in first - const { error: signInError } = await supabase.auth.signInWithPassword({ - email: `${stxAddress}@stacks.id`, - password: stxAddress, - }); - - if (signInError && signInError.status === 400) { - // User doesn't exist, proceed with sign up - toast({ - description: "Creating your account...", - }); - - const { error: signUpError } = await supabase.auth.signUp({ - email: `${stxAddress}@stacks.id`, - password: stxAddress, - }); - - if (signUpError) throw signUpError; - - toast({ - description: "Successfully signed up...", - variant: "default", - }); - - return true; - } else if (signInError) { - throw signInError; - } - - toast({ - description: "Redirecting to dashboard...", - variant: "default", - }); - - return true; - } catch (error) { - console.error("Authentication error:", error); - toast({ - description: "Authentication failed. Please try again.", - variant: "destructive", - }); - return false; - } - }; - const handleAuth = async () => { - setIsLoading(true); - try { - toast({ - description: "Connecting wallet...", - }); - - // Connect wallet - await new Promise((resolve) => { - showConnect({ - appDetails: { - name: "AIBTC Champions Sprint", - icon: window.location.origin + "/logos/aibtcdev-avatar-1000px.png", - }, - onCancel: () => { - toast({ - description: "Wallet connection cancelled.", - }); - setIsLoading(false); - }, - onFinish: () => resolve(), - userSession, - }); - }); - - const userData = userSession.loadUserData(); - const stxAddress = userData.profile.stxAddress.mainnet; - - toast({ - description: "Wallet connected. Authenticating...", - }); - - const success = await handleAuthentication(stxAddress); + const { success } = await connectWallet(); - if (success) { - // Delay redirect to show success message - setTimeout(() => { - router.push("/chat"); - }, 2000); - } - } catch (error) { - console.error("Wallet connection error:", error); - toast({ - description: "Failed to connect wallet. Please try again.", - variant: "destructive", - }); - } finally { - setIsLoading(false); + if (success) { + router.push("/chat"); } }; From 1dea6bed80263621475540203ad9a3a450973728 Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Wed, 11 Dec 2024 10:02:55 +0545 Subject: [PATCH 08/15] fix: refect user data on auth state change --- src/app/application-layout.tsx | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/app/application-layout.tsx b/src/app/application-layout.tsx index 592b6e37..da917d6d 100644 --- a/src/app/application-layout.tsx +++ b/src/app/application-layout.tsx @@ -74,7 +74,24 @@ function AccountDropdownMenu({ export function ApplicationLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname(); - const { data: userData, isLoading } = useUserData(); + const { data: userData, isLoading, refetch } = useUserData(); + + // Add a listener for auth state changes + React.useEffect(() => { + const { data: authListener } = supabase.auth.onAuthStateChange( + async (event, session) => { + if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") { + // Refetch user data when signed in or token is refreshed + await refetch(); + } + } + ); + + // Cleanup subscription + return () => { + authListener.subscription.unsubscribe(); + }; + }, [refetch]); const displayAddress = React.useMemo(() => { if (!userData?.stxAddress) return ""; @@ -137,10 +154,6 @@ export function ApplicationLayout({ children }: { children: React.ReactNode }) { - {/* - - Dashboard - */} Chat @@ -201,17 +214,17 @@ export function ApplicationLayout({ children }: { children: React.ReactNode }) { {userData && !isLoading && ( - + - {isLoading ? "Loading..." : displayAddress} + {displayAddress} - {isLoading ? "Loading..." : displayRole} + {displayRole} From f392b11fe4a709472b6ae1523d90d253f05da9cf Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Wed, 11 Dec 2024 10:36:57 +0545 Subject: [PATCH 09/15] fix: middleware and connect page --- src/app/connect/page.tsx | 49 ++++++++++++++++++++++++++++++ src/components/Home/Home.tsx | 7 +++-- src/hooks/useAuth.ts | 2 +- src/utils/supabase/middleware.ts | 52 +++++++++++++++++--------------- 4 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 src/app/connect/page.tsx diff --git a/src/app/connect/page.tsx b/src/app/connect/page.tsx new file mode 100644 index 00000000..e38f134c --- /dev/null +++ b/src/app/connect/page.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { useSearchParams } from "next/navigation"; +import { useAuth } from "@/hooks/useAuth"; + +export default function ConnectPage() { + const { connectWallet } = useAuth(); + const searchParams = useSearchParams(); + const redirectPath = searchParams.get("redirect") || "/chat"; + const hasInitiatedConnection = useRef(false); + + useEffect(() => { + const initiateConnection = async () => { + if (hasInitiatedConnection.current) return; + hasInitiatedConnection.current = true; + + try { + console.log("Starting connection process"); + console.log("Redirect path:", redirectPath); + + const result = await connectWallet(); + + console.log("Connection result:", result); + + if (result.success) { + console.log("Attempting to redirect to:", redirectPath); + window.location.href = redirectPath; + } else { + console.log("Connection failed, redirecting to home"); + window.location.href = "/"; + } + } catch (error) { + console.error("Error in connection process:", error); + window.location.href = "/"; + } + }; + + initiateConnection(); + }, [connectWallet, redirectPath]); + + return ( +
+
+

Redirecting to {redirectPath}...

+
+
+ ); +} diff --git a/src/components/Home/Home.tsx b/src/components/Home/Home.tsx index b3d78619..463491eb 100644 --- a/src/components/Home/Home.tsx +++ b/src/components/Home/Home.tsx @@ -3,7 +3,7 @@ import React from "react"; import Image from "next/image"; // import Authentication from "../auth/Authentication"; #FOR GITHUB AUTH -import SignIn from "../auth/StacksAuth"; +// import SignIn from "../auth/StacksAuth"; export default function Home() { return ( @@ -17,11 +17,12 @@ export default function Home() { height={400} /> + {/* WE CAN REMOVE THIS NOW */}
{/* Authentication component */} -
+ {/*
-
+
*/}
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 34fd25f4..85e6b309 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -42,7 +42,7 @@ export const useAuth = () => { } toast({ - description: "Redirecting to dashboard...", + description: "Redirecting...", variant: "default", }); diff --git a/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts index 4ebca4aa..25e7c828 100644 --- a/src/utils/supabase/middleware.ts +++ b/src/utils/supabase/middleware.ts @@ -37,13 +37,35 @@ export const updateSession = async (request: NextRequest) => { }, }); + // Protected routes array + const protectedRoutes = [ + "/dashboard", + "/chat", + "/marketplace", + "/profile", + "/admin" + ]; + + // Check if current path is a protected route + const isProtectedRoute = protectedRoutes.some(route => + request.nextUrl.pathname.startsWith(route) + ); + // Get the user const { data: { user }, error: userError, } = await supabase.auth.getUser(); - // If trying to access admin route + // If it's a protected route and there's no user + if (isProtectedRoute && (userError || !user)) { + // Redirect to connect page with original destination + const connectUrl = new URL("/connect", request.url); + connectUrl.searchParams.set('redirect', request.nextUrl.pathname); + return NextResponse.redirect(connectUrl); + } + + // Admin route specific logic if (request.nextUrl.pathname.startsWith("/admin")) { if (userError || !user) { // If no user, redirect to login @@ -63,38 +85,18 @@ export const updateSession = async (request: NextRequest) => { } } - // Regular route protection - if (request.nextUrl.pathname.startsWith("/dashboard") && userError) { - return NextResponse.redirect(new URL("/", request.url)); - } - - // Add chat to protected route - if (request.nextUrl.pathname.startsWith("/chat") && (userError || !user)) { - return NextResponse.redirect(new URL("/", request.url)); - } - - if ( - request.nextUrl.pathname.startsWith("/public-crew") && - (userError || !user) - ) { - return NextResponse.redirect(new URL("/", request.url)); - } - - if(request.nextUrl.pathname.startsWith("/profile")&&(userError || !user)){ - return NextResponse.redirect(new URL("/", request.url)) - } - - if (request.nextUrl.pathname === "/" && !userError) { + // Redirect logged-in users from root to chat + if (request.nextUrl.pathname === "/" && !userError && user) { return NextResponse.redirect(new URL("/chat", request.url)); } return response; } catch (error) { - console.error(error); + console.error("Middleware authentication error:", error); return NextResponse.next({ request: { headers: request.headers, }, }); } -}; +}; \ No newline at end of file From efca7598953b3079b7d10c987d59b6be9cd34612 Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Wed, 11 Dec 2024 10:40:04 +0545 Subject: [PATCH 10/15] fix: build errors --- src/app/application-layout.tsx | 2 +- src/app/providers.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/application-layout.tsx b/src/app/application-layout.tsx index da917d6d..b5e28c99 100644 --- a/src/app/application-layout.tsx +++ b/src/app/application-layout.tsx @@ -79,7 +79,7 @@ export function ApplicationLayout({ children }: { children: React.ReactNode }) { // Add a listener for auth state changes React.useEffect(() => { const { data: authListener } = supabase.auth.onAuthStateChange( - async (event, session) => { + async (event) => { if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") { // Refetch user data when signed in or token is refreshed await refetch(); diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 87e720e7..1579d82f 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -5,7 +5,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ThemeProvider } from "@/components/ui/theme-provider"; import { Toaster } from "@/components/ui/toaster"; import { ApplicationLayout } from "./application-layout"; -import { usePathname } from "next/navigation"; const queryClient = new QueryClient(); From a1caee5580980d7e512dbc223c5abfe41b9bafa9 Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Thu, 12 Dec 2024 11:53:20 +0545 Subject: [PATCH 11/15] fix: redirect to home if wallet connection is cancelled --- src/hooks/useAuth.ts | 3 +++ src/utils/supabase/middleware.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 85e6b309..ebe983e1 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -2,11 +2,13 @@ import { useState } from 'react'; import { AppConfig, showConnect, UserSession } from "@stacks/connect"; import { supabase } from "@/utils/supabase/client"; import { useToast } from "@/hooks/use-toast"; +import { useRouter } from 'next/navigation'; const appConfig = new AppConfig(["store_write", "publish_data"]); const userSession = new UserSession({ appConfig }); export const useAuth = () => { + const router = useRouter() const [isLoading, setIsLoading] = useState(false); const { toast } = useToast(); @@ -75,6 +77,7 @@ export const useAuth = () => { toast({ description: "Wallet connection cancelled.", }); + router.push("/") setIsLoading(false); }, onFinish: () => resolve(), diff --git a/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts index 25e7c828..e2bc1e43 100644 --- a/src/utils/supabase/middleware.ts +++ b/src/utils/supabase/middleware.ts @@ -43,7 +43,8 @@ export const updateSession = async (request: NextRequest) => { "/chat", "/marketplace", "/profile", - "/admin" + "/admin", + "/crews" ]; // Check if current path is a protected route From eebcb036cc62f6126175e2791610647573742b61 Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Thu, 12 Dec 2024 12:14:41 +0545 Subject: [PATCH 12/15] fix: update middleware --- src/utils/supabase/middleware.ts | 75 ++++++++++++++++---------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts index e2bc1e43..43f4fd30 100644 --- a/src/utils/supabase/middleware.ts +++ b/src/utils/supabase/middleware.ts @@ -1,6 +1,15 @@ import { createServerClient } from "@supabase/ssr"; import { type NextRequest, NextResponse } from "next/server"; - +// Define routes with different authentication strategies +const protectedPaths = { + '/dashboard': { type: 'component' }, + '/dashboard/:path*': { type: 'component' }, + '/chat': { type: 'component' }, + '/marketplace': { type: 'component' }, + '/profile': { type: 'component' }, + '/admin': { type: 'redirect' }, + '/admin/:path*': { type: 'redirect' }, +} as const; export const updateSession = async (request: NextRequest) => { try { // Create an unmodified response @@ -9,7 +18,6 @@ export const updateSession = async (request: NextRequest) => { headers: request.headers, }, }); - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; if (!supabaseUrl || !supabaseAnonKey) { @@ -17,7 +25,6 @@ export const updateSession = async (request: NextRequest) => { "middleware: missing supabase url or supabase anon key in env vars" ); } - const supabase = createServerClient(supabaseUrl, supabaseAnonKey, { cookies: { getAll() { @@ -36,61 +43,55 @@ export const updateSession = async (request: NextRequest) => { }, }, }); - - // Protected routes array - const protectedRoutes = [ - "/dashboard", - "/chat", - "/marketplace", - "/profile", - "/admin", - "/crews" - ]; - - // Check if current path is a protected route - const isProtectedRoute = protectedRoutes.some(route => - request.nextUrl.pathname.startsWith(route) - ); - + // Check if current path matches a protected route + const pathname = request.nextUrl.pathname; + const matchedPath = Object.entries(protectedPaths).find(([route, _]) => { + const pattern = new RegExp(`^${route.replace(/\/:path\*/, '(/.*)?').replace(/\//g, '\\/')}$`); + return pattern.test(pathname); + }); // Get the user const { data: { user }, error: userError, } = await supabase.auth.getUser(); - + // Set authentication headers + response.headers.set('x-authenticated', (!userError && !!user) ? 'true' : 'false'); // If it's a protected route and there's no user - if (isProtectedRoute && (userError || !user)) { - // Redirect to connect page with original destination - const connectUrl = new URL("/connect", request.url); - connectUrl.searchParams.set('redirect', request.nextUrl.pathname); - return NextResponse.redirect(connectUrl); + if (matchedPath && (userError || !user)) { + const [_, config] = matchedPath; + switch (config.type) { + case 'redirect': + // Redirect to connect page with original destination + const connectUrl = new URL("/connect", request.url); + connectUrl.searchParams.set('redirect', pathname); + return NextResponse.redirect(connectUrl); + case 'component': + // Allow rendering but mark as unauthorized + response.headers.set('x-auth-status', 'unauthorized'); + break; + } } - // Admin route specific logic - if (request.nextUrl.pathname.startsWith("/admin")) { + if (pathname.startsWith("/admin")) { if (userError || !user) { - // If no user, redirect to login + // If no user, redirect to home return NextResponse.redirect(new URL("/", request.url)); } - // Check user role in profiles table const { data: profileData, error: profileError } = await supabase .from("profiles") .select("role") .eq("id", user.id) .single(); - if (profileError || !profileData || profileData.role !== "Admin") { - // If not admin, redirect to dashboard + // If not admin, redirect to chat return NextResponse.redirect(new URL("/chat", request.url)); } } - - // Redirect logged-in users from root to chat - if (request.nextUrl.pathname === "/" && !userError && user) { - return NextResponse.redirect(new URL("/chat", request.url)); + // Authenticated routes + if (!userError && user) { + response.headers.set('x-auth-status', 'authorized'); } - return response; } catch (error) { console.error("Middleware authentication error:", error); @@ -100,4 +101,4 @@ export const updateSession = async (request: NextRequest) => { }, }); } -}; \ No newline at end of file +}; From 177e142b3d1bc34cf0d575d8b84d3178578f6c67 Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Thu, 12 Dec 2024 12:37:47 +0545 Subject: [PATCH 13/15] fix: update component with latest middleware --- src/app/chat/page.tsx | 21 ++++++++++++++++--- src/app/crews/page.tsx | 17 +++++++++++++++- src/utils/supabase/middleware.ts | 35 +++++++++++++++++++++++++------- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 7fcf2eb4..bca77b06 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -1,10 +1,25 @@ import React from "react"; +import { headers } from "next/headers"; import Chat from "@/components/chat/Chat"; +import Link from "next/link"; const page = () => { - return ( - - ); + const headersList = headers(); + const authStatus = headersList.get("x-auth-status"); + + if (authStatus === "unauthorized") { + return ( +
+
+ {/* THIS IS SHOWN WHEN THE USER IS NOT AUTHENTICATED INSTEAD OF FULL REDIRECT TO CONNECT*/} +

Limited Access

+ connect to access the chat +
+
+ ); + } + // Render chat component if authenticated + return ; }; export default page; diff --git a/src/app/crews/page.tsx b/src/app/crews/page.tsx index 5c648d63..b99e370d 100644 --- a/src/app/crews/page.tsx +++ b/src/app/crews/page.tsx @@ -1,13 +1,28 @@ - import React from "react"; import Crews from "@/components/crews/Crews"; import { Metadata } from "next"; +import { headers } from "next/headers"; +import Link from "next/link"; export const metadata: Metadata = { title: "Crews", }; const page = () => { + const headersList = headers(); + const authStatus = headersList.get("x-auth-status"); + + if (authStatus === "unauthorized") { + return ( +
+
+ {/* THIS IS SHOWN WHEN THE USER IS NOT AUTHENTICATED INSTEAD OF FULL REDIRECT TO CONNECT*/} +

Limited Access

+ connect to access the crews +
+
+ ); + } return ; }; diff --git a/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts index 43f4fd30..4a072162 100644 --- a/src/utils/supabase/middleware.ts +++ b/src/utils/supabase/middleware.ts @@ -1,15 +1,16 @@ import { createServerClient } from "@supabase/ssr"; import { type NextRequest, NextResponse } from "next/server"; + // Define routes with different authentication strategies const protectedPaths = { - '/dashboard': { type: 'component' }, - '/dashboard/:path*': { type: 'component' }, '/chat': { type: 'component' }, + '/crews': { type: 'component' }, '/marketplace': { type: 'component' }, '/profile': { type: 'component' }, '/admin': { type: 'redirect' }, '/admin/:path*': { type: 'redirect' }, } as const; + export const updateSession = async (request: NextRequest) => { try { // Create an unmodified response @@ -18,13 +19,16 @@ export const updateSession = async (request: NextRequest) => { headers: request.headers, }, }); + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + if (!supabaseUrl || !supabaseAnonKey) { throw new Error( - "middleware: missing supabase url or supabase anon key in env vars" + "middleware: missing supabase URL or supabase anon key in env vars" ); } + const supabase = createServerClient(supabaseUrl, supabaseAnonKey, { cookies: { getAll() { @@ -43,55 +47,72 @@ export const updateSession = async (request: NextRequest) => { }, }, }); + // Check if current path matches a protected route const pathname = request.nextUrl.pathname; const matchedPath = Object.entries(protectedPaths).find(([route, _]) => { - const pattern = new RegExp(`^${route.replace(/\/:path\*/, '(/.*)?').replace(/\//g, '\\/')}$`); + const pattern = new RegExp( + `^${route.replace(/\/:path\*/, '(/.*)?').replace(/\//g, '\\/')}$` + ); return pattern.test(pathname); }); + // Get the user const { data: { user }, error: userError, } = await supabase.auth.getUser(); + // Set authentication headers - response.headers.set('x-authenticated', (!userError && !!user) ? 'true' : 'false'); + response.headers.set( + 'x-authenticated', + !userError && !!user ? 'true' : 'false' + ); + // If it's a protected route and there's no user if (matchedPath && (userError || !user)) { const [_, config] = matchedPath; + switch (config.type) { - case 'redirect': + case 'redirect': { // Redirect to connect page with original destination const connectUrl = new URL("/connect", request.url); connectUrl.searchParams.set('redirect', pathname); return NextResponse.redirect(connectUrl); - case 'component': + } + case 'component': { // Allow rendering but mark as unauthorized response.headers.set('x-auth-status', 'unauthorized'); break; + } } } + // Admin route specific logic if (pathname.startsWith("/admin")) { if (userError || !user) { // If no user, redirect to home return NextResponse.redirect(new URL("/", request.url)); } + // Check user role in profiles table const { data: profileData, error: profileError } = await supabase .from("profiles") .select("role") .eq("id", user.id) .single(); + if (profileError || !profileData || profileData.role !== "Admin") { // If not admin, redirect to chat return NextResponse.redirect(new URL("/chat", request.url)); } } + // Authenticated routes if (!userError && user) { response.headers.set('x-auth-status', 'authorized'); } + return response; } catch (error) { console.error("Middleware authentication error:", error); From 39fa46d1fb137230a0f5ceb865edb75889d889d7 Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Thu, 12 Dec 2024 12:45:32 +0545 Subject: [PATCH 14/15] fix: cloudflare build errors --- src/app/chat/page.tsx | 2 ++ src/app/crews/page.tsx | 2 ++ src/utils/supabase/middleware.ts | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index bca77b06..c3a77cff 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -3,6 +3,8 @@ import { headers } from "next/headers"; import Chat from "@/components/chat/Chat"; import Link from "next/link"; +export const runtime = "edge"; + const page = () => { const headersList = headers(); const authStatus = headersList.get("x-auth-status"); diff --git a/src/app/crews/page.tsx b/src/app/crews/page.tsx index b99e370d..38a25b72 100644 --- a/src/app/crews/page.tsx +++ b/src/app/crews/page.tsx @@ -8,6 +8,8 @@ export const metadata: Metadata = { title: "Crews", }; +export const runtime = "edge"; + const page = () => { const headersList = headers(); const authStatus = headersList.get("x-auth-status"); diff --git a/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts index 4a072162..c58cc841 100644 --- a/src/utils/supabase/middleware.ts +++ b/src/utils/supabase/middleware.ts @@ -50,7 +50,7 @@ export const updateSession = async (request: NextRequest) => { // Check if current path matches a protected route const pathname = request.nextUrl.pathname; - const matchedPath = Object.entries(protectedPaths).find(([route, _]) => { + const matchedPath = Object.entries(protectedPaths).find(([route]) => { const pattern = new RegExp( `^${route.replace(/\/:path\*/, '(/.*)?').replace(/\//g, '\\/')}$` ); @@ -71,7 +71,7 @@ export const updateSession = async (request: NextRequest) => { // If it's a protected route and there's no user if (matchedPath && (userError || !user)) { - const [_, config] = matchedPath; + const [, config] = matchedPath; switch (config.type) { case 'redirect': { @@ -123,3 +123,4 @@ export const updateSession = async (request: NextRequest) => { }); } }; + From b10db322d39d5bdb6b865f2e241770fe82f2b345 Mon Sep 17 00:00:00 2001 From: Biwas Bhandari Date: Fri, 13 Dec 2024 13:10:07 +0545 Subject: [PATCH 15/15] fix: middleware and admin route --- src/app/admin/page.tsx | 43 ---------------------------- src/utils/supabase/middleware.ts | 49 ++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 64 deletions(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 3861c1a6..8680932e 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -38,7 +38,6 @@ export default function AdminPanel() { const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const [roleFilter, setRoleFilter] = useState("All"); - const [isAdmin, setIsAdmin] = useState(false); const [editingProfile, setEditingProfile] = useState<{ [key: string]: { assigned_agent_address: string; @@ -49,32 +48,9 @@ export default function AdminPanel() { const [sortOrder, setSortOrder] = useState(null); useEffect(() => { - checkAdminStatus(); fetchProfiles(); }, []); - const checkAdminStatus = async () => { - try { - const { - data: { user }, - } = await supabase.auth.getUser(); - if (!user) throw new Error("Not authenticated"); - - const { data, error } = await supabase - .from("profiles") - .select("role") - .eq("id", user.id) - .single(); - - if (error) throw error; - setIsAdmin(data.role === "Admin"); - } catch (error) { - console.error("Failed to verify admin status:", error); - setError("Failed to verify admin status"); - setIsAdmin(false); - } - }; - const formatEmail = (email: string): string => { return email.split("@")[0].toUpperCase(); }; @@ -123,11 +99,6 @@ export default function AdminPanel() { }; const updateProfile = async (userId: string): Promise => { - if (!isAdmin) { - setError("Only admins can update profiles"); - return; - } - try { setError(null); const updates: Partial = { @@ -221,20 +192,6 @@ export default function AdminPanel() { return ; } - if (!isAdmin) { - return ( - - - - - Access denied. Only administrators can manage profiles. - - - - - ); - } - return ( diff --git a/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts index c58cc841..8be6221d 100644 --- a/src/utils/supabase/middleware.ts +++ b/src/utils/supabase/middleware.ts @@ -69,7 +69,31 @@ export const updateSession = async (request: NextRequest) => { !userError && !!user ? 'true' : 'false' ); - // If it's a protected route and there's no user + // Special handling for admin routes + if (pathname.startsWith("/admin")) { + // If no user immediately redirect to home without showing connect popup + if (userError || !user) { + return NextResponse.redirect(new URL("/", request.url)); + } + + // Check user role in profiles table + const { data: profileData, error: profileError } = await supabase + .from("profiles") + .select("role") + .eq("id", user.id) + .single(); + + // If not admin, redirect to chat + if (profileError || !profileData || profileData.role !== "Admin") { + return NextResponse.redirect(new URL("/chat", request.url)); + } + + // Admin user, allow access + response.headers.set('x-auth-status', 'authorized'); + return response; + } + + // Handle other protected routes if (matchedPath && (userError || !user)) { const [, config] = matchedPath; @@ -88,26 +112,6 @@ export const updateSession = async (request: NextRequest) => { } } - // Admin route specific logic - if (pathname.startsWith("/admin")) { - if (userError || !user) { - // If no user, redirect to home - return NextResponse.redirect(new URL("/", request.url)); - } - - // Check user role in profiles table - const { data: profileData, error: profileError } = await supabase - .from("profiles") - .select("role") - .eq("id", user.id) - .single(); - - if (profileError || !profileData || profileData.role !== "Admin") { - // If not admin, redirect to chat - return NextResponse.redirect(new URL("/chat", request.url)); - } - } - // Authenticated routes if (!userError && user) { response.headers.set('x-auth-status', 'authorized'); @@ -124,3 +128,6 @@ export const updateSession = async (request: NextRequest) => { } }; +export const config = { + matcher: ['/admin/:path*', '/chat', '/crews', '/marketplace', '/profile'], +}; \ No newline at end of file